This commit is contained in:
babayaga 2025-12-28 21:12:09 +01:00
parent 313a81f270
commit ca79656a90
10 changed files with 2494 additions and 12 deletions

View File

@ -61,6 +61,11 @@
"tslog": "^4.9.3",
"unified": "^11.0.5",
"unist-util-visit": "^5.0.0",
"yargs": "^18.0.0"
"tailwindcss": "^4.0.7",
"flowbite": "^3.1.2",
"yargs": "^18.0.0",
"html-escaper": "^3.0.3",
"html-validate": "^8.18.2",
"@types/html-escaper": "^3.0.0"
}
}
}

View File

@ -278,7 +278,6 @@ export const gallery = async (
const meta_path_md = `${mediaPath}/${parts.name}.md`
const meta_markdown = exists(meta_path_md) ? read(meta_path_md, "string") as string : "" as string
console.log('filePath', filePath)
let imageMeta: any = await loadImage(filePath)
let exifRaw: any = null
try {

View File

@ -1,22 +1,22 @@
---
import "../styles/flowbite.css"
// import "../styles/flowbite.css"
import "../styles/global.css"
import "../styles/custom.scss"
// import "../styles/global.css"
// import "../styles/custom.scss"
import { sync as read } from '@polymech/fs/read'
import { AstroSeo } from "@astrolib/seo"
import { default as AstroSeo } from "../seo/AstroSeo.astro"
import { I18N_SOURCE_LANGUAGE } from "../app/config.js"
import { translate } from '@polymech/astro-base/base/i18n.js'
import { item_defaults } from '@/base/index.js'
import { translate } from '../base/i18n.js'
import { item_defaults } from '../base/index.js'
import { LANGUAGES_PROD } from "../app/config.js"
import config from "../app/config.json"
import { plainify } from "@polymech/astro-base/base/strings.js"
import { plainify } from "../base/strings.js"
import StructuredData from '@polymech/astro-base/components/ArticleStructuredData.astro'
import Hreflang from '@polymech/astro-base/components/hreflang.astro'
import StructuredData from '../components/ArticleStructuredData.astro'
import Hreflang from '../components/hreflang.astro'
export interface Props {
title?: string;

View File

@ -0,0 +1,42 @@
---
import type { AstroSeoProps } from "./types";
import { buildTags } from "./utils/buildTags";
export interface Props extends AstroSeoProps {}
const {
title,
titleTemplate,
noindex,
nofollow,
robotsProps,
description,
canonical,
mobileAlternate,
languageAlternates,
openGraph,
facebook,
twitter,
additionalMetaTags,
additionalLinkTags,
} = Astro.props;
---
<Fragment
set:html={buildTags({
title,
titleTemplate,
noindex,
nofollow,
robotsProps,
description,
canonical,
mobileAlternate,
languageAlternates,
openGraph,
facebook,
twitter,
additionalMetaTags,
additionalLinkTags,
})}
/>

View File

@ -0,0 +1 @@

View File

@ -0,0 +1,455 @@
export type OpeningHoursSpecification = {
opens: string;
closes: string;
dayOfWeek: string | string[];
validFrom?: string;
validThrough?: string;
};
export type Offer = {
priceSpecification: PriceSpecification;
itemOffered: Service;
};
export type PriceSpecification = {
type: string;
priceCurrency: string;
price: string;
};
export type Service = {
name: string;
description: string;
};
export type Geo = {
latitude: string;
longitude: string;
};
export type GeoCircle = {
geoMidpoint: Geo;
geoRadius: string;
};
export type Action = {
actionName: string;
actionType: string;
target: string;
};
export type Step = {
type: string;
name: string;
url?: string;
itemListElement?: StepDetails[];
image?: string;
};
export type StepDetails = {
type: "HowToTip" | "HowToDirection";
text: string;
};
export interface Person {
name: string;
}
export interface Answer {
text: string;
dateCreated?: string;
upvoteCount?: number;
url?: string;
author?: Person;
}
export interface Question {
name: string;
answerCount: number;
acceptedAnswer?: Answer;
suggestedAnswer?: Answer[];
text?: string;
author?: Person;
upvoteCount?: number;
dateCreated?: string;
}
export interface Instruction {
name?: string;
text: string;
url?: string;
image?: string;
}
export interface Performer {
type?: "Person" | "PerformingGroup";
name: string;
}
export interface Place {
name: string;
address: Address;
sameAs?: string;
}
export interface VirtualLocation {
name?: string;
sameAs?: string;
url: string;
}
export type Location = Place | VirtualLocation;
export type EventStatus =
| "EventCancelled"
| "EventMovedOnline"
| "EventPostponed"
| "EventRescheduled"
| "EventScheduled";
export type EventAttendanceMode =
| "MixedEventAttendanceMode"
| "OfflineEventAttendanceMode"
| "OnlineEventAttendanceMode";
export interface Organizer {
type: "Person" | "Organization";
name: string;
url: string;
}
export interface ContactPoint {
contactType: string;
telephone: string;
areaServed?: string | string[];
availableLanguage?: string | string[];
contactOption?: string | string[];
}
export interface CreativeWork {
author: string;
about: string;
name: string;
datePublished: string;
audience?: string;
keywords?: string;
thumbnailUrl?: string;
image?: string;
}
export interface Producer {
name: string;
url?: string;
}
export interface ContactPoint {
contactType: string;
telephone: string;
areaServed?: string | string[];
availableLanguage?: string | string[];
contactOption?: string | string[];
}
export interface Question {
questionName: string;
acceptedAnswerText: string;
}
export interface Provider {
type?: "Organization" | "Person";
name: string;
url?: string;
}
export interface ItemListElements {
item: string;
name: string;
position: number;
}
export interface OpenGraphMedia {
url: string;
width?: number;
height?: number;
alt?: string;
type?: string;
secureUrl?: string;
}
export interface Address {
streetAddress: string;
addressLocality: string;
addressRegion?: string;
postalCode: string;
addressCountry: string;
}
export interface Video {
name: string;
description: string;
thumbnailUrls: string[];
uploadDate: string;
contentUrl?: string;
duration?: string;
embedUrl?: string;
expires?: string;
hasPart?: Clip | Clip[];
watchCount?: number;
publication?: BroadcastEvent | BroadcastEvent[];
regionsAllowed?: string | string[];
}
export interface Clip {
name: string;
startOffset: number;
url: string;
}
export interface BroadcastEvent {
name?: string;
isLiveBroadcast: boolean;
startDate: string;
endDate: string;
}
export type Offers = {
price: string;
priceCurrency: string;
priceValidUntil?: string;
itemCondition?: string;
availability?: string;
url?: string;
seller: {
name: string;
};
validFrom?: string;
};
export type AggregateOffer = {
priceCurrency: string;
lowPrice: string;
highPrice?: string;
offerCount?: string;
offers?: Offers | Offers[];
};
export interface OpenGraphVideoActors {
profile: string;
role?: string;
}
export interface OpenGraph {
url?: string;
type?: string;
title?: string;
description?: string;
images?: ReadonlyArray<OpenGraphMedia>;
videos?: ReadonlyArray<OpenGraphMedia>;
defaultImageHeight?: number;
defaultImageWidth?: number;
locale?: string;
site_name?: string;
profile?: OpenGraphProfile;
book?: OpenGraphBook;
article?: OpenGraphArticle;
video?: OpenGraphVideo;
}
export interface OpenGraphProfile {
firstName?: string;
lastName?: string;
username?: string;
gender?: string;
}
export interface OpenGraphBook {
authors?: ReadonlyArray<string>;
isbn?: string;
releaseDate?: string;
tags?: ReadonlyArray<string>;
}
export interface OpenGraphArticle {
publishedTime?: string;
modifiedTime?: string;
expirationTime?: string;
authors?: ReadonlyArray<string>;
section?: string;
tags?: ReadonlyArray<string>;
}
export interface OpenGraphVideo {
actors?: ReadonlyArray<OpenGraphVideoActors>;
directors?: ReadonlyArray<string>;
writers?: ReadonlyArray<string>;
duration?: number;
releaseDate?: string;
tags?: ReadonlyArray<string>;
series?: string;
}
export interface Twitter {
handle?: string;
site?: string;
cardType?: string;
}
interface MobileAlternate {
media: string;
href: string;
}
interface LanguageAlternate {
hreflang: string;
href: string;
}
interface LinkTag {
rel: string;
href: string;
sizes?: string;
media?: string;
type?: string;
color?: string;
as?: string;
crossOrigin?: string;
}
export interface BaseMetaTag {
content: string;
}
export interface HTML5MetaTag extends BaseMetaTag {
name: string;
property?: undefined;
httpEquiv?: undefined;
}
export interface RDFaMetaTag extends BaseMetaTag {
property: string;
name?: undefined;
httpEquiv?: undefined;
}
export interface HTTPEquivMetaTag extends BaseMetaTag {
httpEquiv:
| "content-security-policy"
| "content-type"
| "default-style"
| "x-ua-compatible"
| "refresh";
name?: undefined;
property?: undefined;
}
export type MetaTag = HTML5MetaTag | RDFaMetaTag | HTTPEquivMetaTag;
export type ImagePrevSize = "none" | "standard" | "large";
export type AggregateRating = {
ratingValue: string;
reviewCount?: string;
ratingCount?: string;
bestRating?: string;
};
export type GamePlayMode = "CoOp" | "MultiPlayer" | "SinglePlayer";
export type Review = {
author: string;
datePublished?: string;
reviewBody?: string;
name?: string;
publisher?: Publisher;
reviewRating: ReviewRating;
};
export type ReviewRating = {
bestRating?: string;
ratingValue: string;
worstRating?: string;
};
export type Author = {
type: string;
name: string;
};
export type ArticleAuthor = {
name: string;
url: string;
};
export type Publisher = {
type: string;
name: string;
};
export type ReviewedBy = {
type?: string;
name: string;
};
export type ApplicationCategory =
| "Game"
| "SocialNetworking"
| "Travel"
| "Shopping"
| "Sports"
| "Lifestyle"
| "Business"
| "Design"
| "Developer"
| "Driver"
| "Educational"
| "Health"
| "Finance"
| "Security"
| "Browser"
| "Communication"
| "DesktopEnhancement"
| "Entertainment"
| "Multimedia"
| "Home"
| "Utilities"
| "Reference";
export type OrganizationCategory =
| "Airline"
| "Consortium"
| "Corporation"
| "EducationalOrganization"
| "FundingScheme"
| "GovernmentOrganization"
| "LibrarySystem"
| "LocalBusiness"
| "MedicalOrganization"
| "NGO"
| "NewsMediaOrganization"
| "PerformingGroup"
| "Project"
| "ResearchOrganization"
| "SportsOrganization"
| "WorkersUnion"
| "Organization";
export interface AdditionalRobotsProps {
nosnippet?: boolean;
maxSnippet?: number;
maxImagePreview?: ImagePrevSize;
maxVideoPreview?: number;
noarchive?: boolean;
unavailableAfter?: string;
noimageindex?: boolean;
notranslate?: boolean;
}
export interface AstroSeoProps {
title?: string;
titleTemplate?: string;
noindex?: boolean;
nofollow?: boolean;
robotsProps?: AdditionalRobotsProps;
description?: string;
canonical?: string;
mobileAlternate?: MobileAlternate;
languageAlternates?: ReadonlyArray<LanguageAlternate>;
openGraph?: OpenGraph;
facebook?: { appId: string };
twitter?: Twitter;
additionalMetaTags?: ReadonlyArray<MetaTag>;
additionalLinkTags?: ReadonlyArray<LinkTag>;
}

View File

@ -0,0 +1,457 @@
import { escape } from "html-escaper";
import type { AstroSeoProps, OpenGraphMedia } from "../types.js";
const createMetaTag = (attributes: Record<string, string>): string => {
const attrs = Object.entries(attributes)
.map(([key, value]) => `${key}="${escape(value)}"`)
.join(" ");
return `<meta ${attrs}>`;
};
const createLinkTag = (attributes: Record<string, string>): string => {
const attrs = Object.entries(attributes)
.map(([key, value]) => `${key}="${escape(value)}"`)
.join(" ");
return `<link ${attrs}>`;
};
const createOpenGraphTag = (property: string, content: string): string => {
return createMetaTag({ property: `og:${property}`, content });
};
const buildOpenGraphMediaTags = (
mediaType: "image" | "video",
media: ReadonlyArray<OpenGraphMedia>
): string => {
let tags = "";
const addTag = (tag: string) => {
tags += tag + "\n";
};
media.forEach((medium) => {
addTag(createOpenGraphTag(mediaType, medium.url));
if (medium.alt) {
addTag(createOpenGraphTag(`${mediaType}:alt`, medium.alt));
}
if (medium.secureUrl) {
addTag(createOpenGraphTag(`${mediaType}:secure_url`, medium.secureUrl));
}
if (medium.type) {
addTag(createOpenGraphTag(`${mediaType}:type`, medium.type));
}
if (medium.width) {
addTag(createOpenGraphTag(`${mediaType}:width`, medium.width.toString()));
}
if (medium.height) {
addTag(
createOpenGraphTag(`${mediaType}:height`, medium.height.toString())
);
}
});
return tags;
};
export const buildTags = (config: AstroSeoProps): string => {
let tagsToRender = "";
const addTag = (tag: string) => {
tagsToRender += tag + "\n";
};
const addMetaTag = (attributes: Record<string, string>) => {
addTag(
`<meta ${Object.entries(attributes)
.map(([key, value]) => `${key}="${escape(value)}"`)
.join(" ")} />`
);
};
const addLinkTag = (attributes: Record<string, string>) => {
addTag(
`<link ${Object.entries(attributes)
.map(([key, value]) => `${key}="${escape(value)}"`)
.join(" ")} />`
);
};
const addOpenGraphTag = (property: string, content: string) => {
addMetaTag({ property: `og:${property}`, content });
};
// Title
if (config.title) {
const formattedTitle = config.titleTemplate
? config.titleTemplate.replace("%s", config.title)
: config.title;
addTag(`<title>${escape(formattedTitle)}</title>`);
}
// Description
if (config.description) {
addTag(createMetaTag({ name: "description", content: config.description }));
}
// Robots: noindex, nofollow, and other robotsProps
let robotsContent: string[] = [];
if (typeof config.noindex !== "undefined") {
robotsContent.push(config.noindex ? "noindex" : "index");
}
if (typeof config.nofollow !== "undefined") {
robotsContent.push(config.nofollow ? "nofollow" : "follow");
}
if (config.robotsProps) {
const {
nosnippet,
maxSnippet,
maxImagePreview,
noarchive,
unavailableAfter,
noimageindex,
notranslate,
} = config.robotsProps;
if (nosnippet) robotsContent.push("nosnippet");
if (typeof maxSnippet === 'number') robotsContent.push(`max-snippet:${maxSnippet}`);
if (maxImagePreview)
robotsContent.push(`max-image-preview:${maxImagePreview}`);
if (noarchive) robotsContent.push("noarchive");
if (unavailableAfter)
robotsContent.push(`unavailable_after:${unavailableAfter}`);
if (noimageindex) robotsContent.push("noimageindex");
if (notranslate) robotsContent.push("notranslate");
}
if (robotsContent.length > 0) {
addTag(createMetaTag({ name: "robots", content: robotsContent.join(",") }));
}
// Canonical
if (config.canonical) {
addTag(createLinkTag({ rel: "canonical", href: config.canonical }));
}
// Mobile Alternate
if (config.mobileAlternate) {
addTag(
createLinkTag({
rel: "alternate",
media: config.mobileAlternate.media,
href: config.mobileAlternate.href,
})
);
}
// Language Alternates
if (config.languageAlternates && config.languageAlternates.length > 0) {
config.languageAlternates.forEach((languageAlternate) => {
addTag(
createLinkTag({
rel: "alternate",
hreflang: languageAlternate.hreflang,
href: languageAlternate.href,
})
);
});
}
// OpenGraph
if (config.openGraph) {
const title = config.openGraph?.title || config.title;
if (title) {
addTag(createOpenGraphTag("title", title));
}
const description = config.openGraph?.description || config.description;
if (description) {
addTag(createOpenGraphTag("description", description));
}
if (config.openGraph.url) {
addTag(createOpenGraphTag("url", config.openGraph.url));
}
if (config.openGraph.type) {
addTag(createOpenGraphTag("type", config.openGraph.type));
}
if (config.openGraph.images && config.openGraph.images.length) {
addTag(buildOpenGraphMediaTags("image", config.openGraph.images));
}
if (config.openGraph.videos && config.openGraph.videos.length) {
addTag(buildOpenGraphMediaTags("video", config.openGraph.videos));
}
if (config.openGraph.locale) {
addTag(createOpenGraphTag("locale", config.openGraph.locale));
}
if (config.openGraph.site_name) {
addTag(createOpenGraphTag("site_name", config.openGraph.site_name));
}
// Open Graph Profile
if (config.openGraph.profile) {
if (config.openGraph.profile.firstName) {
addTag(
createOpenGraphTag(
"profile:first_name",
config.openGraph.profile.firstName
)
);
}
if (config.openGraph.profile.lastName) {
addTag(
createOpenGraphTag(
"profile:last_name",
config.openGraph.profile.lastName
)
);
}
if (config.openGraph.profile.username) {
addTag(
createOpenGraphTag(
"profile:username",
config.openGraph.profile.username
)
);
}
if (config.openGraph.profile.gender) {
addTag(
createOpenGraphTag("profile:gender", config.openGraph.profile.gender)
);
}
}
// Open Graph Book
if (config.openGraph.book) {
if (
config.openGraph.book.authors &&
config.openGraph.book.authors.length
) {
config.openGraph.book.authors.forEach((author) => {
addTag(createOpenGraphTag("book:author", author));
});
}
if (config.openGraph.book.isbn) {
addTag(createOpenGraphTag("book:isbn", config.openGraph.book.isbn));
}
if (config.openGraph.book.releaseDate) {
addTag(
createOpenGraphTag(
"book:release_date",
config.openGraph.book.releaseDate
)
);
}
if (config.openGraph.book.tags && config.openGraph.book.tags.length) {
config.openGraph.book.tags.forEach((tag) => {
addTag(createOpenGraphTag("book:tag", tag));
});
}
}
// Open Graph Article
if (config.openGraph.article) {
if (config.openGraph.article.publishedTime) {
addTag(
createOpenGraphTag(
"article:published_time",
config.openGraph.article.publishedTime
)
);
}
if (config.openGraph.article.modifiedTime) {
addTag(
createOpenGraphTag(
"article:modified_time",
config.openGraph.article.modifiedTime
)
);
}
if (config.openGraph.article.expirationTime) {
addTag(
createOpenGraphTag(
"article:expiration_time",
config.openGraph.article.expirationTime
)
);
}
if (
config.openGraph.article.authors &&
config.openGraph.article.authors.length
) {
config.openGraph.article.authors.forEach((author) => {
addTag(createOpenGraphTag("article:author", author));
});
}
if (config.openGraph.article.section) {
addTag(
createOpenGraphTag(
"article:section",
config.openGraph.article.section
)
);
}
if (
config.openGraph.article.tags &&
config.openGraph.article.tags.length
) {
config.openGraph.article.tags.forEach((tag) => {
addTag(createOpenGraphTag("article:tag", tag));
});
}
}
// Open Graph Video
if (config.openGraph.video) {
if (
config.openGraph.video.actors &&
config.openGraph.video.actors.length
) {
config.openGraph.video.actors.forEach((actor) => {
addTag(createOpenGraphTag("video:actor", actor.profile));
if (actor.role) {
addTag(createOpenGraphTag("video:actor:role", actor.role));
}
});
}
if (
config.openGraph.video.directors &&
config.openGraph.video.directors.length
) {
config.openGraph.video.directors.forEach((director) => {
addTag(createOpenGraphTag("video:director", director));
});
}
if (
config.openGraph.video.writers &&
config.openGraph.video.writers.length
) {
config.openGraph.video.writers.forEach((writer) => {
addTag(createOpenGraphTag("video:writer", writer));
});
}
if (config.openGraph.video.duration) {
addTag(
createOpenGraphTag(
"video:duration",
config.openGraph.video.duration.toString()
)
);
}
if (config.openGraph.video.releaseDate) {
addTag(
createOpenGraphTag(
"video:release_date",
config.openGraph.video.releaseDate
)
);
}
if (config.openGraph.video.tags && config.openGraph.video.tags.length) {
config.openGraph.video.tags.forEach((tag) => {
addTag(createOpenGraphTag("video:tag", tag));
});
}
if (config.openGraph.video.series) {
addTag(
createOpenGraphTag("video:series", config.openGraph.video.series)
);
}
}
}
// Facebook
if (config.facebook && config.facebook.appId) {
addTag(
createMetaTag({ property: "fb:app_id", content: config.facebook.appId })
);
}
// Twitter
if (config.twitter) {
if (config.twitter.cardType) {
addTag(
createMetaTag({
name: "twitter:card",
content: config.twitter.cardType,
})
);
}
if (config.twitter.site) {
addTag(
createMetaTag({ name: "twitter:site", content: config.twitter.site })
);
}
if (config.twitter.handle) {
addTag(
createMetaTag({
name: "twitter:creator",
content: config.twitter.handle,
})
);
}
}
// Additional Meta Tags
if (config.additionalMetaTags && config.additionalMetaTags.length > 0) {
config.additionalMetaTags.forEach((metaTag) => {
const attributes: Record<string, string> = {
content: metaTag.content,
};
if ("name" in metaTag && metaTag.name) {
attributes.name = metaTag.name;
} else if ("property" in metaTag && metaTag.property) {
attributes.property = metaTag.property;
} else if ("httpEquiv" in metaTag && metaTag.httpEquiv) {
attributes["http-equiv"] = metaTag.httpEquiv;
}
addTag(createMetaTag(attributes));
});
}
// Additional Link Tags
if (config.additionalLinkTags && config.additionalLinkTags.length > 0) {
config.additionalLinkTags.forEach((linkTag) => {
const attributes: Record<string, string> = {
rel: linkTag.rel,
href: linkTag.href,
};
if (linkTag.sizes) {
attributes.sizes = linkTag.sizes;
}
if (linkTag.media) {
attributes.media = linkTag.media;
}
if (linkTag.type) {
attributes.type = linkTag.type;
}
if (linkTag.color) {
attributes.color = linkTag.color;
}
if (linkTag.as) {
attributes.as = linkTag.as;
}
if (linkTag.crossOrigin) {
attributes.crossorigin = linkTag.crossOrigin;
}
addTag(createLinkTag(attributes));
});
}
return tagsToRender.trim();
};

View File

@ -0,0 +1,118 @@
.p-20 {
padding: 0px;
}
.content {
UL,
LI {
list-style-type: inherit;
padding: inherit;
margin: inherit;
}
}
.bullets {
list-style-type: disc;
padding-left: 1rem;
}
.bullets li {
list-style-type: inherit;
}
.bullets ul {
list-style-type: inherit;
}
.specs TABLE {
width: 100%;
border-collapse: collapse;
}
.specs TABLE thead {
display: none;
}
.specs .table TR td:last-child {
text-align: right;
}
.table td+td,
.table th+th {
text-align: right
}
.widget-table {
width: 100%
}
.widget-table table>tbody>tr>td:nth-of-type(1) {
/*background: #f1f1f1;*/
font-weight: 500;
text-align: left;
}
.widget-table tbody tr:nth-child(even) {
background-color: #00c3ff17;
}
.widget-table table {
width: 100%
}
.widget-table table td {
border: 0px solid #dfdfdf;
color: #808080;
line-height: 1.4;
position: relative;
}
.widget-table table tbody td {
padding: 4px;
text-align: right;
}
.widget-table table thead tr th {
padding: 4px;
text-align: right;
}
.border {
border-style: var(--tw-border-style);
border-width: 1px;
border-color: #dfdfdf86;
}
.mySwiper .swiper-slide {
text-align: center;
}
.mySlide {}
.swiper-slide img {
display: block;
width: 100%;
height: 100%;
object-fit: contain;
}
.main-image {
background-image: none !important;
width: auto !important;
}
.lightbox {
display: none;
}
.max-w-9\/10 {
max-width: 90%;
}
.max-h-9\/10 {
max-height: 90%;
}
.imagetools-img{
background-image: none !important;
}

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff