seo:item keywords

This commit is contained in:
lovebird 2025-03-08 09:14:54 +01:00
parent 7a456b508d
commit 8c9192e27a
8 changed files with 180 additions and 62 deletions

View File

@ -1 +1,19 @@
{"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"}
{
"name": "",
"short_name": "",
"icons": [
{
"src": "/favicons/android-chrome-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/favicons/android-chrome-512x512.png",
"sizes": "512x512",
"type": "image/png"
}
],
"theme_color": "#ffffff",
"background_color": "#ffffff",
"display": "standalone"
}

View File

@ -88,8 +88,8 @@
"pages":{
"home":{
"hero": "https://assets.osr-plastic.org/machines//assets/newsletter/common/products/extruders/overview-3.jpg",
"_blog":{
"store": "posts"
"blog":{
"store": "resources"
}
}
}

45
src/base/seo.ts Normal file
View File

@ -0,0 +1,45 @@
import { unified } from "unified";
import remarkParse from "remark-parse";
import remarkRehype from "remark-rehype";
import rehypeStringify from "rehype-stringify";
import { createComponent } from "astro/runtime/server/astro-component.js";
import { renderTemplate, unescapeHTML } from "astro/runtime/server/index.js";
import { findUp } from 'find-up'
import { createLogger } from '@polymech/log'
import { parse, IProfile, IComponentConfig } from '@polymech/commons'
import { renderMarkup } from "@/model/component.js";
import { sync as read } from '@polymech/fs/read'
import { sync as exists } from '@polymech/fs/exists'
import {
LOGGING_NAMESPACE,
OSRL_ENV,
OSRL_PRODUCT_PROFILE,
PRODUCT_ROOT,
I18N_SOURCE_LANGUAGE
} from 'config/config.js'
import { translate } from '@/base/i18n.js'
import { item_defaults } from '@/base/index.js'
import config from "../app/config.json" with { "type": "json" }
export const item_keywords = async (item: IComponentConfig, locale: string = I18N_SOURCE_LANGUAGE) => {
let system_keywords = ""
if (item.PRODUCT_ROOT) {
const defaultsJson = await item_defaults(item.PRODUCT_ROOT);
let defaults: Record<string, string> = defaultsJson ? read(defaultsJson, 'json') as Record<string, string> || {} : {}
const defaultsKeywords = (defaults.keywords || "").split(',').map(k => k.trim()).filter(Boolean)
const configKeywords = (config.metadata.keywords || "").split(',').map(k => k.trim()).filter(Boolean)
const itemKeywords = (item.keywords || "").split(',').map(k => k.trim()).filter(Boolean)
const allKeywords = Array.from(new Set([
...defaultsKeywords,
...configKeywords,
...itemKeywords
])).join(',');
system_keywords = await translate(allKeywords, I18N_SOURCE_LANGUAGE, locale)
}
const keywordsArray = [...new Set([item.name, ...system_keywords.split(',')])]
const keywords = keywordsArray.join(',')
return keywords
}

View File

@ -1,23 +1,20 @@
---
import "../styles/flowbite.css"
import { I18N_SOURCE_LANGUAGE, isRTL } from "config/config.js"
import { translate } from '@/base/i18n.js'
import { item_defaults } from '@/base/index.js'
import { LANGUAGES_PROD, PRODUCT_ROOT } from "config/config.js"
import { LANGUAGES_PROD } from "config/config.js"
import config from "config/config.json"
import { plainify } from "../base/strings.js"
import "../styles/global.css"
import "../styles/custom.scss"
import { sync as read } from '@polymech/fs/read'
import { sync as exists } from '@polymech/fs/exists'
import { AstroSeo } from "@astrolib/seo"
import { item_keywords } from '@/base/seo.js'
import StructuredData from './head/ArticleStructuredData.astro'
import Hreflang from '@/components/polymech/hreflang.astro'
import { IComponentConfig } from "@polymech/commons"
export interface Props {
title?: string;
@ -28,8 +25,6 @@ export interface Props {
canonical?: string;
view: string;
path: string;
additionalKeywords?: string[];
onKeywordsProcessed?: (keywords: string[]) => void;
frontmatter?: {
title?: string;
description?: string;
@ -38,51 +33,27 @@ export interface Props {
};
}
const env = import.meta.env
const { frontmatter, view, path, additionalKeywords = [], onKeywordsProcessed } = Astro.props
const { frontmatter, view, path } = Astro.props as Props;
const item: IComponentConfig = frontmatter as IComponentConfig || {}
const { url } = Astro.request
const REDIRECT = false //import.meta.env.I18N_REDIRECT
const REDIRECT = false
const _url = Astro.url
const canonicalUrl = _url.origin
const canonicalURL = new URL(Astro.url.pathname, Astro.site)
const locale = Astro.currentLocale || I18N_SOURCE_LANGUAGE
const hreflangs = LANGUAGES_PROD.filter((lang)=>lang!==Astro.currentLocale).map((lang) => ({
lang,
url: `${canonicalUrl}/${lang}/${view}/${path}`,
}))
const image = frontmatter?.image || config.site.image
const image = item?.image || config.site.image
const image_url = image.url
const image_alt = image.alt
const title = frontmatter?.title || config.site.title
const description = frontmatter?.description || config.metadata.description
const title = item?.title as string || config.site.title || ""
const description = item?.description as string || config.metadata.description
let system_keywords = ""
const item_config = frontmatter as any || {}
if(item_config.PRODUCT_ROOT){
const defaultsJson = await item_defaults(item_config.PRODUCT_ROOT);
let defaults:Record<string,string> = read(defaultsJson, 'json') || {}
const defaultsKeywords = (defaults.keywords || "").split(',').map(k => k.trim()).filter(Boolean)
const configKeywords = (config.metadata.keywords || "").split(',').map(k => k.trim()).filter(Boolean)
const itemKeywords = (item_config.keywords || "").split(',').map(k => k.trim()).filter(Boolean)
const allKeywords = Array.from(new Set([
...defaultsKeywords,
...configKeywords,
...itemKeywords,
...additionalKeywords
])).join(',');
system_keywords = await translate(allKeywords, I18N_SOURCE_LANGUAGE, locale)
}
const keywordsArray = [...new Set([item_config.name, ...system_keywords.split(','), ...additionalKeywords])]
const keywords = keywordsArray.join(',')
// Call the callback with the processed keywords if it exists
if (typeof onKeywordsProcessed === 'function') {
onKeywordsProcessed(keywordsArray);
}
const keywords = await item_keywords(item, Astro.currentLocale)
---
<AstroSeo
@ -118,31 +89,33 @@ if (typeof onKeywordsProcessed === 'function') {
<!-- Canonical URL -->
<Hreflang canonical={canonicalURL} hreflangs={hreflangs} />
<!-- meta-description -->
<meta name="description" content={plainify(description)}/>
<!-- meta-keywords -->
<meta name="keywords" content={plainify( keywords )}/>
<meta name="author" content={plainify( "" )} />
<meta name="author" content={plainify( config?.metadata?.author )} />
<!-- Favicons -->
<link rel="icon" href="/favicon.ico" sizes="any" />
<link rel="icon" href="/favicons/favicon.ico" sizes="any" />
<link rel="icon" href="/icon.svg" type="image/svg+xml" />
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
<link rel="manifest" href="/manifest.webmanifest" />
<link rel="apple-touch-icon" href="/favicons/apple-touch-icon.png" />
<link rel="manifest" href="/favicons/site.webmanifest" />
<!-- Favicon for IE -->
<link rel="shortcut icon" href="/favicon.ico" type="image/x-icon" />
<link rel="shortcut icon" href="/favicons/favicon.ico" type="image/x-icon" />
<!-- Favicons for different sizes -->
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" />
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
<link rel="icon" type="image/png" sizes="48x48" href="/favicon-48x48.png" />
<link rel="icon" type="image/png" sizes="16x16" href="/favicons/favicon-16x16.png" />
<link rel="icon" type="image/png" sizes="32x32" href="/favicons/favicon-32x32.png" />
<!-- Additional SEO -->
<meta name="robots" content="index, follow" />
<meta name="googlebot" content="index, follow" />
<!-- Apple Touch Icon (already included in favicons, but keeping for backwards compatibility) -->
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
<link rel="apple-touch-icon" sizes="180x180" href="/favicons/apple-touch-icon.png" />
<!-- Theme Color for Mobile Browsers -->
<meta name="theme-color" content="#ffffff" />
@ -162,15 +135,16 @@ if (typeof onKeywordsProcessed === 'function') {
/>
<script is:inline src="https://cdn.jsdelivr.net/npm/flowbite@3.0.0/dist/flowbite.min.js"></script>
<!-- alpine JS -->
<script src="//unpkg.com/alpinejs" defer></script>
<StructuredData frontmatter={frontmatter} />
<StructuredData frontmatter={item} />
{ REDIRECT && <script>
const currentPath = window.location.pathname;
if (!/^\/[a-z]{2}(\/|$)/i.test(currentPath)) {
let language = navigator.language || navigator.userLanguage
let language = navigator.language
language = language.split('-')[0]
language = "en"
window.location.href = `/${language}`;

View File

@ -15,7 +15,6 @@ interface Image {
export interface Props {
images: Image[];
id?: string;
onAltTextsProcessed?: (altTexts: string[]) => void;
siteKeywords?: string[];
gallerySettings?: {
SIZES_REGULAR?: string;
@ -33,7 +32,7 @@ export interface Props {
};
}
const { images, gallerySettings = {}, lightboxSettings = {}, onAltTextsProcessed, siteKeywords = [] } = Astro.props;
const { images, gallerySettings = {}, lightboxSettings = {}, siteKeywords = [] } = Astro.props;
const mergedGallerySettings = {
SIZES_REGULAR: gallerySettings.SIZES_REGULAR || IMAGE_SETTINGS.GALLERY.SIZES_REGULAR,
@ -74,11 +73,6 @@ const translatedAltTexts = await Promise.all(
})
);
// Pass the translated alt texts to the parent component if callback exists
if (onAltTextsProcessed && typeof onAltTextsProcessed === 'function') {
onAltTextsProcessed(translatedAltTexts.filter(Boolean));
}
---
<div

View File

@ -0,0 +1,65 @@
import { translate } from "@/base/i18n.js"
import config from "config/config.json"
}
/**
* Process and combine keywords from multiple sources, translate them, and trigger an optional callback
*
* @param options - Configuration for processing keywords
* @param options.itemConfig - The frontmatter or item configuration object
* @param options.sourceLanguage - The source language for translation
* @param options.targetLocale - The target locale for translation
* @param options.additionalKeywords - Extra keywords to add to the processed list
* @param options.onKeywordsProcessed - Optional callback that receives the processed keywords array
* @returns Object containing the processed keywords as both an array and comma-separated string
*/
export async function processKeywords({
itemConfig = {},
sourceLanguage,
targetLocale,
additionalKeywords = [],
onKeywordsProcessed
}: {
itemConfig?: Record<string, any>;
sourceLanguage: string;
targetLocale: string;
additionalKeywords?: string[];
onKeywordsProcessed?: (keywords: string[]) => void;
}) {
let systemKeywords = "";
if (itemConfig.PRODUCT_ROOT) {
const defaultsJson = await item_defaults(itemConfig.PRODUCT_ROOT);
const defaults: Record<string, string> = await import('@polymech/fs/read').then(m => m.sync(defaultsJson, 'json')) || {};
// Extract keywords from different sources
const defaultsKeywords = (defaults.keywords || "").split(',').map(k => k.trim()).filter(Boolean);
const configKeywords = (config.metadata?.keywords || "").split(',').map(k => k.trim()).filter(Boolean);
const itemKeywords = (itemConfig.keywords || "").split(',').map(k => k.trim()).filter(Boolean);
// Combine and deduplicate all keywords
const allKeywords = Array.from(new Set([
...defaultsKeywords,
...configKeywords,
...itemKeywords,
...additionalKeywords
])).join(',');
// Translate the keywords
systemKeywords = await translate(allKeywords, sourceLanguage, targetLocale);
}
// Final processing and deduplication
const keywordsArray = [...new Set([itemConfig.name, ...systemKeywords.split(','), ...additionalKeywords])].filter(Boolean);
const keywords = keywordsArray.join(',');
// Call the callback with the processed keywords if it exists
if (typeof onKeywordsProcessed === 'function') {
onKeywordsProcessed(keywordsArray);
}
return {
keywordsArray,
keywords
};
}

View File

@ -0,0 +1,21 @@
import { processKeywords } from '@/base/index.js'
const item_config = frontmatter as any || {}
// Process keywords using the extracted function
import { item_defaults, processKeywords } from '@/base/index.js'
itemConfig: item_config,
sourceLanguage: I18N_SOURCE_LANGUAGE,
targetLocale: locale,
additionalKeywords,
onKeywordsProcessed
})
// Process keywords using the extracted function
const item_config = frontmatter as any || {}
const { keywords, keywordsArray } = await processKeywords({
itemConfig: item_config,
sourceLanguage: I18N_SOURCE_LANGUAGE,
targetLocale: locale,
additionalKeywords,
onKeywordsProcessed
})

View File

@ -16,6 +16,7 @@
"skipLibCheck": true,
"baseUrl": ".",
"lib": ["DOM", "ES2015"],
"resolveJsonModule": true,
"paths": {
"@/*": ["src/*"],
"config/*": ["src/app/*"]