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;
|
success: boolean;
|
||||||
order: IOrderSummary;
|
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 interface IProduct {
|
||||||
|
|
||||||
export default interface IProduct {
|
|
||||||
product_no: number;
|
product_no: number;
|
||||||
name: string;
|
name: string;
|
||||||
subtext?: string;
|
subtext?: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
image: string;
|
image: string;
|
||||||
currency: string;
|
|
||||||
variations?: IProductVariation[];
|
|
||||||
primary_color?: string;
|
primary_color?: string;
|
||||||
variation_count?: string;
|
|
||||||
sum_stock?: number;
|
|
||||||
updated?: Date;
|
updated?: Date;
|
||||||
created?: 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 VippsHurtigkasse from '$lib/components/VippsHurtigkasse.svelte';
|
||||||
import { cart } from '$lib/cartStore';
|
import { cart } from '$lib/cartStore';
|
||||||
|
|
||||||
import type IProduct from '$lib/interfaces/IProduct';
|
import type { IProduct } from '$lib/interfaces/IProduct';
|
||||||
|
|
||||||
function postOrder(event: any) {
|
function postOrder(event: any) {
|
||||||
const formData = new FormData(event.target);
|
const formData = new FormData(event.target);
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
import { mockProducts } from '$lib/utils/mock';
|
import { mockProducts } from '$lib/utils/mock';
|
||||||
import type { PageServerData } from './$types';
|
import type { PageServerData } from './$types';
|
||||||
import type IProduct from '$lib/interfaces/IProduct';
|
import type { IProduct } from '$lib/interfaces/IProduct';
|
||||||
|
|
||||||
function subTotal(products: Array<IProduct>) {
|
function subTotal(products: Array<IProduct>) {
|
||||||
let total = 0;
|
let total = 0;
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { dev } from '$app/environment';
|
import { dev } from '$app/environment';
|
||||||
import { env } from '$env/dynamic/private';
|
import { env } from '$env/dynamic/private';
|
||||||
|
import type { IProductsResponse } from '$lib/interfaces/ApiResponse';
|
||||||
import type { PageServerLoad } from './$types';
|
import type { PageServerLoad } from './$types';
|
||||||
|
|
||||||
export const load: PageServerLoad = async ({ fetch }) => {
|
export const load: PageServerLoad = async ({ fetch }) => {
|
||||||
@@ -9,7 +10,7 @@ export const load: PageServerLoad = async ({ fetch }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const res = await fetch(url);
|
const res = await fetch(url);
|
||||||
const products = await res.json();
|
const products: IProductsResponse = await res.json();
|
||||||
|
|
||||||
return products;
|
return products;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { PageData } from './$types';
|
import type { PageData } from './$types';
|
||||||
import ProductTile from './ProductTile.svelte';
|
import ProductTile from './ProductTile.svelte';
|
||||||
import type IProduct from '$lib/interfaces/IProduct';
|
import type { IProduct } from '$lib/interfaces/IProduct';
|
||||||
|
|
||||||
export let data: PageData;
|
export let data: PageData;
|
||||||
const products = data.products as Array<IProduct>;
|
const products = data.products as Array<IProduct>;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type IProduct from '$lib/interfaces/IProduct';
|
import type { IProduct } from '$lib/interfaces/IProduct';
|
||||||
|
|
||||||
export let product: IProduct;
|
export let product: IProduct;
|
||||||
export let large = false;
|
export let large = false;
|
||||||
@@ -12,14 +12,16 @@
|
|||||||
product?.primary_color === '#231B1D' ? '#f3efeb' : '#37301e'
|
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}`}">
|
<div class="{`image-frame ${large ? 'large' : null}`}">
|
||||||
<img src="{product?.image}" alt="{product?.name}" />
|
<img src="{product?.image}" alt="{product?.name}" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if !large}
|
{#if !large}
|
||||||
<p class="subtext">{product?.subtext}</p>
|
<p class="subtext">{product?.subtext}</p>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { dev } from '$app/environment';
|
import { dev } from '$app/environment';
|
||||||
import { env } from '$env/dynamic/private';
|
import { env } from '$env/dynamic/private';
|
||||||
|
import type { IProductResponse } from '$lib/interfaces/ApiResponse';
|
||||||
import type { PageServerLoad } from './$types';
|
import type { PageServerLoad } from './$types';
|
||||||
|
|
||||||
export const load: PageServerLoad = async ({ fetch, params }) => {
|
export const load: PageServerLoad = async ({ fetch, params }) => {
|
||||||
@@ -11,6 +12,6 @@ export const load: PageServerLoad = async ({ fetch, params }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const res = await fetch(url);
|
const res = await fetch(url);
|
||||||
const product = await res.json();
|
const product: IProductResponse = await res.json();
|
||||||
return product;
|
return product;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { onMount } from 'svelte'
|
||||||
import type { PageData } from './$types';
|
import type { PageData } from './$types';
|
||||||
import ProductTile from '../ProductTile.svelte';
|
import ProductTile from '../ProductTile.svelte';
|
||||||
import ProductVariationSelect from '$lib/components/ProductVariationSelect.svelte';
|
import ProductVariationSelect from '$lib/components/ProductVariationSelect.svelte';
|
||||||
import QuantitySelect from '$lib/components/QuantitySelect.svelte';
|
import QuantitySelect from '$lib/components/QuantitySelect.svelte';
|
||||||
import SizesSection from './SizesSection.svelte';
|
import SizesSection from './SizesSection.svelte';
|
||||||
import type IProduct from '$lib/interfaces/IProduct';
|
import type { IProduct, IVariation } from '$lib/interfaces/IProduct';
|
||||||
import type IProductVariation from '$lib/interfaces/IProductVariation';
|
|
||||||
import Button from '$lib/components/Button.svelte';
|
import Button from '$lib/components/Button.svelte';
|
||||||
|
import generateProductJsonLd from '$lib/jsonld/product';
|
||||||
|
|
||||||
export let data: PageData;
|
export let data: PageData;
|
||||||
const product = data.product as IProduct;
|
const product = data.product as IProduct;
|
||||||
@@ -28,12 +29,19 @@
|
|||||||
cooldownInputs = false;
|
cooldownInputs = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function defaultVariation() {
|
||||||
|
return product.variations?.find(variation => variation.default_price)
|
||||||
|
}
|
||||||
|
|
||||||
|
let jsonLd: HTMLElement;
|
||||||
let cooldownInputs = false;
|
let cooldownInputs = false;
|
||||||
let quantity = 1;
|
let quantity = 1;
|
||||||
let selectedVariation: IProductVariation;
|
let selectedVariation: IVariation | undefined = defaultVariation()
|
||||||
$: addProductButtonText = cooldownInputs
|
$: addProductButtonText = cooldownInputs
|
||||||
? `${quantity} produkt${quantity > 1 ? 'er' : ''} lagt til`
|
? `${quantity} produkt${quantity > 1 ? 'er' : ''} lagt til`
|
||||||
: `Legg til ${quantity} i handlekurven`;
|
: `Legg til ${quantity} i handlekurven`;
|
||||||
|
|
||||||
|
onMount(() => document.head.appendChild(generateProductJsonLd(product)))
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="product-container">
|
<div class="product-container">
|
||||||
|
|||||||
@@ -41,18 +41,16 @@ export async function GET() {
|
|||||||
return new Response(body, { headers });
|
return new Response(body, { headers });
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildSitemapUrl(address: string, modified: string, frequency: string) {
|
function buildSitemapUrl(path: string, modified: string, frequency: string) {
|
||||||
return `<url>
|
return `<url>
|
||||||
<loc>https://${address}</loc>
|
<loc>https://${domain}${path}</loc>
|
||||||
<lastmod>${modified}</lastmod>
|
<lastmod>${modified}</lastmod>
|
||||||
<changefreq>${frequency}</changefreq>
|
<changefreq>${frequency}</changefreq>
|
||||||
</url>`;
|
</url>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function sitemapPages(): string {
|
function sitemapPages(): string {
|
||||||
return pages
|
return pages.map((page) => buildSitemapUrl(`/${page.name}`, page.modified, 'yearly')).join('\n');
|
||||||
.map((page) => buildSitemapUrl(`https://${domain}/${page.name}`, page.modified, 'yearly'))
|
|
||||||
.join('\n');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function sitemapShopPages(): Promise<string> {
|
async function sitemapShopPages(): Promise<string> {
|
||||||
@@ -66,11 +64,7 @@ async function sitemapShopPages(): Promise<string> {
|
|||||||
|
|
||||||
return products?.products
|
return products?.products
|
||||||
?.map((product) =>
|
?.map((product) =>
|
||||||
buildSitemapUrl(
|
buildSitemapUrl(`/shop/${product.product_no}`, String(product.updated), 'daily')
|
||||||
`https://${domain}/shop/${product.product_no}`,
|
|
||||||
String(product.updated),
|
|
||||||
'daily'
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
.join('\n');
|
.join('\n');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
import ProductList from './WarehouseProductList.svelte';
|
import ProductList from './WarehouseProductList.svelte';
|
||||||
import type IProduct from '$lib/interfaces/IProduct';
|
|
||||||
import Button from '$lib/components/Button.svelte';
|
import Button from '$lib/components/Button.svelte';
|
||||||
|
import type { IProduct } from '$lib/interfaces/IProduct';
|
||||||
import type { PageData } from './$types';
|
import type { PageData } from './$types';
|
||||||
|
|
||||||
export let data: PageData;
|
export let data: PageData;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
import type IProduct from '$lib/interfaces/IProduct';
|
import type { IProduct } from '$lib/interfaces/IProduct';
|
||||||
|
|
||||||
export let products: Array<IProduct>;
|
export let products: Array<IProduct>;
|
||||||
|
|
||||||
@@ -29,10 +29,10 @@
|
|||||||
|
|
||||||
<td class="name-and-price">
|
<td class="name-and-price">
|
||||||
<p><a href="/warehouse/{product.product_no}">{product.name}</a></p>
|
<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>
|
||||||
|
|
||||||
<td class="stock-column">{product.sum_stock}</td>
|
<td class="stock-column">{product?.sum_stock}</td>
|
||||||
|
|
||||||
<td class="date-column"
|
<td class="date-column"
|
||||||
>{new Intl.DateTimeFormat('nb-NO', { dateStyle: 'short', timeStyle: 'short' }).format(
|
>{new Intl.DateTimeFormat('nb-NO', { dateStyle: 'short', timeStyle: 'short' }).format(
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
import DetailsSection from './DetailsSection.svelte';
|
import DetailsSection from './DetailsSection.svelte';
|
||||||
import Button from '$lib/components/Button.svelte';
|
import Button from '$lib/components/Button.svelte';
|
||||||
import type { PageServerData } from './$types';
|
import type { PageServerData } from './$types';
|
||||||
import type IProduct from '$lib/interfaces/IProduct';
|
import type { IProduct } from '$lib/interfaces/IProduct';
|
||||||
|
|
||||||
export let data: PageServerData;
|
export let data: PageServerData;
|
||||||
const product = data.product as IProduct;
|
const product = data.product as IProduct;
|
||||||
@@ -20,7 +20,7 @@
|
|||||||
<img src="{product.image}" alt="Product" />
|
<img src="{product.image}" alt="Product" />
|
||||||
<div class="name-and-price">
|
<div class="name-and-price">
|
||||||
<p>{product.name}</p>
|
<p>{product.name}</p>
|
||||||
<p>NOK {product.price}</p>
|
<p>NOK {product?.price}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="edit-button">
|
<div class="edit-button">
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type IProduct from '$lib/interfaces/IProduct';
|
import type { IProduct } from '$lib/interfaces/IProduct';
|
||||||
|
|
||||||
export let product: IProduct;
|
export let product: IProduct;
|
||||||
export let edit: boolean;
|
export let edit: boolean;
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type IProduct from '$lib/interfaces/IProduct';
|
import type { IProduct, IVariation } from '$lib/interfaces/IProduct';
|
||||||
import type IProductVariation from '$lib/interfaces/IProductVariation';
|
|
||||||
import Button from '$lib/components/Button.svelte';
|
import Button from '$lib/components/Button.svelte';
|
||||||
import Badge from '$lib/components/Badge.svelte';
|
import Badge from '$lib/components/Badge.svelte';
|
||||||
import BadgeType from '$lib/interfaces/BadgeType';
|
import BadgeType from '$lib/interfaces/BadgeType';
|
||||||
@@ -10,7 +9,7 @@
|
|||||||
let editingVariationIndex = -1;
|
let editingVariationIndex = -1;
|
||||||
|
|
||||||
interface ISkuResponse {
|
interface ISkuResponse {
|
||||||
skus: IProductVariation[];
|
skus: IVariation[];
|
||||||
success: boolean;
|
success: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -19,7 +18,7 @@
|
|||||||
product.variations = response?.skus;
|
product.variations = response?.skus;
|
||||||
};
|
};
|
||||||
|
|
||||||
function setDefault(variation: IProductVariation) {
|
function setDefault(variation: IVariation) {
|
||||||
if (!product.variations) return;
|
if (!product.variations) return;
|
||||||
|
|
||||||
let url = `/api/product/${product.product_no}/sku/${variation.sku_id}/default_price`;
|
let url = `/api/product/${product.product_no}/sku/${variation.sku_id}/default_price`;
|
||||||
@@ -60,7 +59,7 @@
|
|||||||
.then(() => (editingVariationIndex = product.variations.length - 1));
|
.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}`;
|
let url = `/api/product/${product.product_no}/sku/${variation?.sku_id}`;
|
||||||
if (window?.location?.href.includes('localhost')) {
|
if (window?.location?.href.includes('localhost')) {
|
||||||
url = 'http://localhost:30010'.concat(url);
|
url = 'http://localhost:30010'.concat(url);
|
||||||
@@ -81,7 +80,7 @@
|
|||||||
.then(() => resetEditingIndex());
|
.then(() => resetEditingIndex());
|
||||||
}
|
}
|
||||||
|
|
||||||
function deleteVariation(variation: IProductVariation) {
|
function deleteVariation(variation: IVariation) {
|
||||||
console.log('delete it using api', variation);
|
console.log('delete it using api', variation);
|
||||||
let url = `/api/product/${product.product_no}/sku/${variation?.sku_id}`;
|
let url = `/api/product/${product.product_no}/sku/${variation?.sku_id}`;
|
||||||
if (window?.location?.href.includes('localhost')) {
|
if (window?.location?.href.includes('localhost')) {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Input from '$lib/components/Input.svelte';
|
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';
|
import type { PageServerData } from './$types';
|
||||||
|
|
||||||
export let data: PageServerData;
|
export let data: PageServerData;
|
||||||
@@ -12,9 +12,9 @@
|
|||||||
<div>
|
<div>
|
||||||
<h2>Product attributes</h2>
|
<h2>Product attributes</h2>
|
||||||
<Input label="Name" value="{product.name}" required="{false}" />
|
<Input label="Name" value="{product.name}" required="{false}" />
|
||||||
<Input label="Description" value="{product.description}" required="{false}" />
|
<Input label="Description" value="{product.description || ''}" required="{false}" />
|
||||||
<Input label="Subtext" value="{product.subtext}" required="{false}" />
|
<Input label="Subtext" value="{product.subtext || ''}" required="{false}" />
|
||||||
<Input label="Color" value="{product.primary_color}" required="{false}" />
|
<Input label="Color" value="{product.primary_color || ''}" required="{false}" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
Reference in New Issue
Block a user