@press2ai/theme-specialist-glossy 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +70 -0
- package/package.json +53 -0
- package/src/ai.ts +111 -0
- package/src/components/Hero.astro +11 -0
- package/src/components/Layout.astro +14 -0
- package/src/components/ProfileArticle.astro +11 -0
- package/src/components/ProfileCard.astro +12 -0
- package/src/index.ts +11 -0
- package/src/schema.ts +58 -0
- package/src/styles/glossy.css +498 -0
- package/src/templates/helpers.ts +10 -0
- package/src/templates/hero.ts +22 -0
- package/src/templates/index.ts +5 -0
- package/src/templates/layout.ts +65 -0
- package/src/templates/profile-article.ts +94 -0
- package/src/templates/profile-card.ts +23 -0
package/README.md
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# @press2ai/theme-specialist-glossy
|
|
2
|
+
|
|
3
|
+
Classless, AI-first theme. Semantic HTML + Schema.org + JSON-LD. Works with **Hono**, **Astro**, or any framework that outputs HTML.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @press2ai/theme-specialist-glossy
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage — Hono / Cloudflare Workers
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
import { layout, profileCard, profileArticle } from '@press2ai/theme-specialist-glossy/templates';
|
|
15
|
+
|
|
16
|
+
app.get('/', (c) => c.html(layout({ title: 'Katalog' }, cards)));
|
|
17
|
+
app.get('/:slug', (c) => c.html(layout({ title: name }, profileArticle(profile))));
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Usage — Astro
|
|
21
|
+
|
|
22
|
+
```astro
|
|
23
|
+
---
|
|
24
|
+
import Layout from '@press2ai/theme-specialist-glossy/Layout.astro';
|
|
25
|
+
import ProfileArticle from '@press2ai/theme-specialist-glossy/ProfileArticle.astro';
|
|
26
|
+
import '@press2ai/theme-specialist-glossy/styles/glossy.css';
|
|
27
|
+
---
|
|
28
|
+
<Layout title={name}>
|
|
29
|
+
<ProfileArticle profile={profile} />
|
|
30
|
+
</Layout>
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Exports
|
|
34
|
+
|
|
35
|
+
| Path | What |
|
|
36
|
+
|---|---|
|
|
37
|
+
| `@press2ai/theme-specialist-glossy` | `profileSchema`, `Profile` type, `getJsonLd`, `getAiManifest` |
|
|
38
|
+
| `@press2ai/theme-specialist-glossy/templates` | `layout`, `hero`, `profileCard`, `profileArticle` |
|
|
39
|
+
| `@press2ai/theme-specialist-glossy/styles/glossy.css` | Classless CSS (~12 KB, no JS) |
|
|
40
|
+
| `@press2ai/theme-specialist-glossy/*.astro` | Astro wrappers (thin, call templates internally) |
|
|
41
|
+
|
|
42
|
+
## Architecture
|
|
43
|
+
|
|
44
|
+
```
|
|
45
|
+
CSS (stripe.css) ← classless, styles by semantic tags
|
|
46
|
+
↑
|
|
47
|
+
templates/*.ts ← pure functions → HTML strings (single source of truth)
|
|
48
|
+
↑
|
|
49
|
+
components/*.astro ← thin wrappers for Astro DX
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
One set of templates, zero divergence between frameworks.
|
|
53
|
+
|
|
54
|
+
## Hosting compatibility
|
|
55
|
+
|
|
56
|
+
| Platform | Runtime | What you get |
|
|
57
|
+
|---|---|---|
|
|
58
|
+
| **GitHub Pages** (Astro) | Static / SSG | Pre-built HTML + CSS. Zero cost, zero backend. Profile pages generated at build time from `profile.json`. |
|
|
59
|
+
| **Cloudflare Workers** (Hono) | Edge SSR | Dynamic HTML from D1/KV. Database queries, opt-out forms, API endpoints (`catalog.json`, `llms.txt`). Free tier: 100k req/day. |
|
|
60
|
+
| **Cloudflare Pages** (Astro SSR) | Edge SSR | Astro with `@astrojs/cloudflare` adapter. D1/KV bindings via `Astro.locals`. Middle ground — Astro DX + edge database. |
|
|
61
|
+
|
|
62
|
+
**GitHub Pages** = static profiles, no server needed. Good for claimed/verified personal pages.
|
|
63
|
+
|
|
64
|
+
**Cloudflare Workers** = full SSR, database, forms, API. Good for catalogs, directories, dynamic listings. Templates work directly — `c.html(layout(...))`.
|
|
65
|
+
|
|
66
|
+
**Cloudflare Pages** = if you want Astro's routing/islands AND server-side data. Same templates under the hood, Astro components on top.
|
|
67
|
+
|
|
68
|
+
## License
|
|
69
|
+
|
|
70
|
+
MIT
|
package/package.json
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@press2ai/theme-specialist-glossy",
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "Classless, AI-first theme inspired by Stripe. Framework-agnostic templates (Hono, Astro, raw HTML). Semantic HTML, Schema.org microdata, JSON-LD — built for LLM crawlers.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"homepage": "https://github.com/press2ai/theme-specialist-glossy",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/press2ai/theme-specialist-glossy.git"
|
|
11
|
+
},
|
|
12
|
+
"keywords": [
|
|
13
|
+
"astro",
|
|
14
|
+
"hono",
|
|
15
|
+
"cloudflare-workers",
|
|
16
|
+
"classless",
|
|
17
|
+
"semantic",
|
|
18
|
+
"ai",
|
|
19
|
+
"llm",
|
|
20
|
+
"schema-org",
|
|
21
|
+
"jsonld"
|
|
22
|
+
],
|
|
23
|
+
"files": [
|
|
24
|
+
"src",
|
|
25
|
+
"README.md"
|
|
26
|
+
],
|
|
27
|
+
"publishConfig": {
|
|
28
|
+
"access": "public"
|
|
29
|
+
},
|
|
30
|
+
"exports": {
|
|
31
|
+
".": "./src/index.ts",
|
|
32
|
+
"./templates": "./src/templates/index.ts",
|
|
33
|
+
"./Layout.astro": "./src/components/Layout.astro",
|
|
34
|
+
"./ProfileArticle.astro": "./src/components/ProfileArticle.astro",
|
|
35
|
+
"./ProfileCard.astro": "./src/components/ProfileCard.astro",
|
|
36
|
+
"./Hero.astro": "./src/components/Hero.astro",
|
|
37
|
+
"./styles/glossy.css": "./src/styles/glossy.css",
|
|
38
|
+
"./schema": "./src/schema.ts",
|
|
39
|
+
"./ai": "./src/ai.ts"
|
|
40
|
+
},
|
|
41
|
+
"peerDependencies": {
|
|
42
|
+
"astro": "^5.0.0"
|
|
43
|
+
},
|
|
44
|
+
"peerDependenciesMeta": {
|
|
45
|
+
"astro": {
|
|
46
|
+
"optional": true
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
"dependencies": {
|
|
50
|
+
"zod": "^3.23.0",
|
|
51
|
+
"zod-to-json-schema": "^3.25.2"
|
|
52
|
+
}
|
|
53
|
+
}
|
package/src/ai.ts
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { zodToJsonSchema } from 'zod-to-json-schema';
|
|
2
|
+
import { profileSchema, type Profile } from './schema.ts';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* AI + Human first hybrid layer.
|
|
6
|
+
*
|
|
7
|
+
* Generates the machine-facing manifests that sit alongside the HTML
|
|
8
|
+
* and make every site equally consumable by humans and AI agents.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export function getJsonSchema() {
|
|
12
|
+
return zodToJsonSchema(profileSchema, {
|
|
13
|
+
name: 'Profile',
|
|
14
|
+
$refStrategy: 'none',
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* /.well-known/ai-profile — discovery manifest for AI agents.
|
|
20
|
+
* Tells an LLM crawler what this site offers and where to find it.
|
|
21
|
+
*/
|
|
22
|
+
export function getAiManifest(opts: {
|
|
23
|
+
baseUrl: string;
|
|
24
|
+
profile: Profile;
|
|
25
|
+
}) {
|
|
26
|
+
const { baseUrl, profile } = opts;
|
|
27
|
+
const base = baseUrl.replace(/\/$/, '');
|
|
28
|
+
const fullName = `${profile.firstName} ${profile.lastName}`;
|
|
29
|
+
|
|
30
|
+
return {
|
|
31
|
+
$schema: 'https://press2ai.dev/schemas/ai-profile/v1.json',
|
|
32
|
+
version: 1,
|
|
33
|
+
name: fullName,
|
|
34
|
+
type: 'Person',
|
|
35
|
+
summary: profile.tagline ?? profile.jobTitle,
|
|
36
|
+
locale: 'pl',
|
|
37
|
+
|
|
38
|
+
representations: [
|
|
39
|
+
{ mediaType: 'text/html', href: `${base}/`, role: 'human' },
|
|
40
|
+
{ mediaType: 'application/json', href: `${base}/.well-known/ai-profile.json`, role: 'manifest' },
|
|
41
|
+
{ mediaType: 'application/json', href: `${base}/profile.json`, role: 'data' },
|
|
42
|
+
{ mediaType: 'application/schema+json', href: `${base}/schema.json`, role: 'schema' },
|
|
43
|
+
{ mediaType: 'text/plain', href: `${base}/llms.txt`, role: 'llm-pointer' },
|
|
44
|
+
{ mediaType: 'application/xml', href: `${base}/sitemap-index.xml`, role: 'sitemap' },
|
|
45
|
+
],
|
|
46
|
+
|
|
47
|
+
schema: `${base}/schema.json`,
|
|
48
|
+
data: `${base}/profile.json`,
|
|
49
|
+
|
|
50
|
+
contact: {
|
|
51
|
+
email: profile.email,
|
|
52
|
+
phone: profile.phone,
|
|
53
|
+
website: profile.website,
|
|
54
|
+
},
|
|
55
|
+
|
|
56
|
+
actions: [
|
|
57
|
+
// v0.3.0 will add MCP-style action endpoints here
|
|
58
|
+
],
|
|
59
|
+
|
|
60
|
+
powered_by: {
|
|
61
|
+
name: '@press2ai/theme-specialist-glossy',
|
|
62
|
+
version: '0.3.0',
|
|
63
|
+
homepage: 'https://github.com/press2ai/theme-specialist-glossy',
|
|
64
|
+
},
|
|
65
|
+
|
|
66
|
+
updated_at: profile.updatedAt ?? new Date().toISOString(),
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Build a JSON-LD Schema.org Person object from a profile.
|
|
72
|
+
*/
|
|
73
|
+
export function getJsonLd(profile: Profile, baseUrl: string) {
|
|
74
|
+
const base = baseUrl.replace(/\/$/, '');
|
|
75
|
+
const fullName = `${profile.firstName} ${profile.lastName}`;
|
|
76
|
+
return {
|
|
77
|
+
'@context': 'https://schema.org',
|
|
78
|
+
'@type': 'Person',
|
|
79
|
+
'@id': `${base}/#person`,
|
|
80
|
+
name: fullName,
|
|
81
|
+
jobTitle: profile.jobTitle,
|
|
82
|
+
description: profile.bio,
|
|
83
|
+
image: profile.photo,
|
|
84
|
+
address: profile.city
|
|
85
|
+
? {
|
|
86
|
+
'@type': 'PostalAddress',
|
|
87
|
+
addressLocality: profile.city,
|
|
88
|
+
addressCountry: profile.country,
|
|
89
|
+
}
|
|
90
|
+
: undefined,
|
|
91
|
+
knowsAbout: profile.specialties,
|
|
92
|
+
knowsLanguage: profile.languages,
|
|
93
|
+
telephone: profile.phone,
|
|
94
|
+
email: profile.email,
|
|
95
|
+
url: profile.website,
|
|
96
|
+
sameAs: Object.entries(profile.social ?? {})
|
|
97
|
+
.filter(([, v]) => v)
|
|
98
|
+
.map(([net, handle]) => {
|
|
99
|
+
const prefixes: Record<string, string> = {
|
|
100
|
+
linkedin: 'https://linkedin.com/in/',
|
|
101
|
+
youtube: 'https://youtube.com/@',
|
|
102
|
+
tiktok: 'https://tiktok.com/@',
|
|
103
|
+
facebook: 'https://facebook.com/',
|
|
104
|
+
twitter: 'https://x.com/',
|
|
105
|
+
instagram: 'https://instagram.com/',
|
|
106
|
+
};
|
|
107
|
+
return `${prefixes[net] ?? `https://${net}.com/`}${handle}`;
|
|
108
|
+
}),
|
|
109
|
+
mainEntityOfPage: `${base}/`,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { layout, type LayoutProps } from '../templates/layout.ts';
|
|
3
|
+
|
|
4
|
+
interface Props extends LayoutProps {}
|
|
5
|
+
|
|
6
|
+
const props = Astro.props;
|
|
7
|
+
const body = await Astro.slots.render('default');
|
|
8
|
+
const headExtra = Astro.slots.has('head') ? await Astro.slots.render('head') : props.headExtra;
|
|
9
|
+
const navExtra = Astro.slots.has('nav') ? await Astro.slots.render('nav') : props.navExtra;
|
|
10
|
+
const footerContent = Astro.slots.has('footer') ? await Astro.slots.render('footer') : props.footerContent;
|
|
11
|
+
|
|
12
|
+
const html = layout({ ...props, headExtra, navExtra, footerContent }, body);
|
|
13
|
+
---
|
|
14
|
+
<Fragment set:html={html} />
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
---
|
|
2
|
+
import type { Profile } from '../schema.ts';
|
|
3
|
+
import { profileCard } from '../templates/profile-card.ts';
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
profile: Profile;
|
|
7
|
+
href: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const html = profileCard(Astro.props.profile, Astro.props.href);
|
|
11
|
+
---
|
|
12
|
+
<Fragment set:html={html} />
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export { profileSchema, type Profile } from './schema.ts';
|
|
2
|
+
export { getJsonSchema, getAiManifest, getJsonLd } from './ai.ts';
|
|
3
|
+
// Framework-agnostic templates (Hono, Cloudflare Workers, plain Node):
|
|
4
|
+
// import { layout, hero, profileCard, profileArticle } from '@press2ai/theme-specialist-glossy/templates';
|
|
5
|
+
//
|
|
6
|
+
// Astro components (thin wrappers over templates):
|
|
7
|
+
// import Layout from '@press2ai/theme-specialist-glossy/Layout.astro';
|
|
8
|
+
// import ProfileArticle from '@press2ai/theme-specialist-glossy/ProfileArticle.astro';
|
|
9
|
+
//
|
|
10
|
+
// CSS (classless, works with any framework):
|
|
11
|
+
// import '@press2ai/theme-specialist-glossy/styles/glossy.css';
|
package/src/schema.ts
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Generic Person profile schema for AI-first landing pages.
|
|
5
|
+
* Designed for trainers, doctors, lawyers, freelancers, coaches — anyone
|
|
6
|
+
* who has a personal brand and wants a semantic, machine-readable site.
|
|
7
|
+
*/
|
|
8
|
+
export const profileSchema = z.object({
|
|
9
|
+
// Core identity
|
|
10
|
+
firstName: z.string(),
|
|
11
|
+
lastName: z.string(),
|
|
12
|
+
jobTitle: z.string(),
|
|
13
|
+
tagline: z.string().optional(),
|
|
14
|
+
bio: z.string().optional(),
|
|
15
|
+
photo: z.string().url().optional(),
|
|
16
|
+
|
|
17
|
+
// Location
|
|
18
|
+
city: z.string().optional(),
|
|
19
|
+
country: z.string().default('PL'),
|
|
20
|
+
|
|
21
|
+
// Specialties / skills
|
|
22
|
+
specialties: z.array(z.string()).default([]),
|
|
23
|
+
languages: z.array(z.string()).default([]),
|
|
24
|
+
|
|
25
|
+
// Contact
|
|
26
|
+
phone: z.string().optional(),
|
|
27
|
+
email: z.string().email().optional(),
|
|
28
|
+
website: z.string().url().optional(),
|
|
29
|
+
social: z
|
|
30
|
+
.object({
|
|
31
|
+
instagram: z.string().optional(),
|
|
32
|
+
facebook: z.string().optional(),
|
|
33
|
+
linkedin: z.string().optional(),
|
|
34
|
+
twitter: z.string().optional(),
|
|
35
|
+
youtube: z.string().optional(),
|
|
36
|
+
tiktok: z.string().optional(),
|
|
37
|
+
})
|
|
38
|
+
.partial()
|
|
39
|
+
.default({}),
|
|
40
|
+
|
|
41
|
+
// Business
|
|
42
|
+
business: z
|
|
43
|
+
.object({
|
|
44
|
+
name: z.string().optional(),
|
|
45
|
+
taxId: z.string().optional(), // NIP
|
|
46
|
+
registryId: z.string().optional(), // REGON
|
|
47
|
+
classification: z.array(z.string()).default([]), // PKD
|
|
48
|
+
})
|
|
49
|
+
.partial()
|
|
50
|
+
.default({}),
|
|
51
|
+
|
|
52
|
+
// Metadata
|
|
53
|
+
source: z.enum(['manual', 'ceidg', 'claimed']).default('manual'),
|
|
54
|
+
verified: z.boolean().default(false),
|
|
55
|
+
updatedAt: z.string().optional(),
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
export type Profile = z.infer<typeof profileSchema>;
|
|
@@ -0,0 +1,498 @@
|
|
|
1
|
+
/* ============================================================
|
|
2
|
+
Otwarty Trener — theme: "stripe"
|
|
3
|
+
Classless. Stylowanie po tagach semantycznych.
|
|
4
|
+
Inspiracja: Stripe / Revolut — clean, bold, dużo whitespace.
|
|
5
|
+
============================================================ */
|
|
6
|
+
|
|
7
|
+
@import url('https://rsms.me/inter/inter.css');
|
|
8
|
+
|
|
9
|
+
:root {
|
|
10
|
+
--font-sans: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
|
11
|
+
--font-display: 'Inter', sans-serif;
|
|
12
|
+
|
|
13
|
+
--color-bg: #ffffff;
|
|
14
|
+
--color-bg-soft: #f6f9fc;
|
|
15
|
+
--color-text: #0a2540;
|
|
16
|
+
--color-text-soft: #425466;
|
|
17
|
+
--color-text-muted: #8898aa;
|
|
18
|
+
--color-border: #e3e8ee;
|
|
19
|
+
--color-border-strong: #cfd7df;
|
|
20
|
+
|
|
21
|
+
--color-accent: #635bff;
|
|
22
|
+
--color-accent-hover: #4f46e5;
|
|
23
|
+
--color-accent-soft: #f0f0ff;
|
|
24
|
+
|
|
25
|
+
--gradient-hero: linear-gradient(150deg, #f6f9fc 0%, #eef2ff 40%, #ffffff 100%);
|
|
26
|
+
--gradient-accent: linear-gradient(135deg, #635bff 0%, #00d4ff 100%);
|
|
27
|
+
|
|
28
|
+
--radius: 12px;
|
|
29
|
+
--radius-lg: 20px;
|
|
30
|
+
--shadow-sm: 0 1px 3px rgba(10, 37, 64, 0.05), 0 1px 2px rgba(10, 37, 64, 0.03);
|
|
31
|
+
--shadow-md: 0 4px 12px rgba(10, 37, 64, 0.06), 0 2px 4px rgba(10, 37, 64, 0.04);
|
|
32
|
+
--shadow-lg: 0 12px 32px rgba(10, 37, 64, 0.08), 0 4px 12px rgba(10, 37, 64, 0.04);
|
|
33
|
+
|
|
34
|
+
--container: 1100px;
|
|
35
|
+
--container-prose: 760px;
|
|
36
|
+
--transition: 180ms cubic-bezier(0.4, 0, 0.2, 1);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
* { box-sizing: border-box; }
|
|
40
|
+
|
|
41
|
+
html {
|
|
42
|
+
font-size: 16px;
|
|
43
|
+
-webkit-font-smoothing: antialiased;
|
|
44
|
+
-moz-osx-font-smoothing: grayscale;
|
|
45
|
+
scroll-behavior: smooth;
|
|
46
|
+
overflow-x: hidden;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
body {
|
|
50
|
+
margin: 0;
|
|
51
|
+
overflow-x: clip;
|
|
52
|
+
font-family: var(--font-sans);
|
|
53
|
+
font-feature-settings: 'cv11', 'ss01', 'ss03';
|
|
54
|
+
color: var(--color-text);
|
|
55
|
+
background: var(--color-bg);
|
|
56
|
+
line-height: 1.6;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/* ---------- LAYOUT ---------- */
|
|
60
|
+
|
|
61
|
+
body > header {
|
|
62
|
+
position: sticky;
|
|
63
|
+
top: 0;
|
|
64
|
+
z-index: 50;
|
|
65
|
+
background: rgba(255, 255, 255, 0.85);
|
|
66
|
+
backdrop-filter: saturate(180%) blur(12px);
|
|
67
|
+
-webkit-backdrop-filter: saturate(180%) blur(12px);
|
|
68
|
+
border-bottom: 1px solid var(--color-border);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
body > header nav {
|
|
72
|
+
max-width: var(--container);
|
|
73
|
+
margin: 0 auto;
|
|
74
|
+
padding: 1rem 1.5rem;
|
|
75
|
+
display: flex;
|
|
76
|
+
align-items: center;
|
|
77
|
+
justify-content: space-between;
|
|
78
|
+
gap: 2rem;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
body > header nav a {
|
|
82
|
+
color: var(--color-text);
|
|
83
|
+
text-decoration: none;
|
|
84
|
+
font-weight: 500;
|
|
85
|
+
font-size: 0.95rem;
|
|
86
|
+
transition: color var(--transition);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
body > header nav a:hover {
|
|
90
|
+
color: var(--color-accent);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
body > header nav a strong {
|
|
94
|
+
font-size: 1.1rem;
|
|
95
|
+
font-weight: 700;
|
|
96
|
+
background: var(--gradient-accent);
|
|
97
|
+
-webkit-background-clip: text;
|
|
98
|
+
background-clip: text;
|
|
99
|
+
color: transparent;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
main {
|
|
103
|
+
max-width: var(--container);
|
|
104
|
+
margin: 0 auto;
|
|
105
|
+
padding: 3rem 1.5rem 5rem;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
body > footer {
|
|
109
|
+
border-top: 1px solid var(--color-border);
|
|
110
|
+
background: var(--color-bg-soft);
|
|
111
|
+
padding: 2.5rem 1.5rem;
|
|
112
|
+
text-align: center;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
body > footer small {
|
|
116
|
+
color: var(--color-text-muted);
|
|
117
|
+
font-size: 0.875rem;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
body > footer a {
|
|
121
|
+
color: var(--color-accent);
|
|
122
|
+
text-decoration: none;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/* ---------- TYPOGRAPHY ---------- */
|
|
126
|
+
|
|
127
|
+
h1, h2, h3, h4 {
|
|
128
|
+
font-family: var(--font-display);
|
|
129
|
+
color: var(--color-text);
|
|
130
|
+
letter-spacing: -0.02em;
|
|
131
|
+
line-height: 1.2;
|
|
132
|
+
margin: 0 0 1rem;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
h1 {
|
|
136
|
+
font-size: clamp(2.25rem, 5vw, 3.5rem);
|
|
137
|
+
font-weight: 800;
|
|
138
|
+
letter-spacing: -0.035em;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
h2 {
|
|
142
|
+
font-size: 1.75rem;
|
|
143
|
+
font-weight: 700;
|
|
144
|
+
margin-top: 2.5rem;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
h3 {
|
|
148
|
+
font-size: 1.25rem;
|
|
149
|
+
font-weight: 600;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
p {
|
|
153
|
+
color: var(--color-text-soft);
|
|
154
|
+
margin: 0 0 1rem;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
a {
|
|
158
|
+
color: var(--color-accent);
|
|
159
|
+
text-decoration: none;
|
|
160
|
+
transition: color var(--transition);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
a:hover {
|
|
164
|
+
color: var(--color-accent-hover);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
small {
|
|
168
|
+
color: var(--color-text-muted);
|
|
169
|
+
font-size: 0.875rem;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
strong {
|
|
173
|
+
color: var(--color-text);
|
|
174
|
+
font-weight: 600;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/* ---------- HERO (hgroup) — Stripe-style mesh gradient ---------- */
|
|
178
|
+
|
|
179
|
+
hgroup {
|
|
180
|
+
position: relative;
|
|
181
|
+
padding-top: 7rem;
|
|
182
|
+
padding-bottom: 9rem;
|
|
183
|
+
padding-left: max(1.5rem, calc((100vw - var(--container)) / 2));
|
|
184
|
+
padding-right: max(1.5rem, calc((100vw - var(--container)) / 2));
|
|
185
|
+
margin: -3rem calc(50% - 50vw) 4rem;
|
|
186
|
+
text-align: left;
|
|
187
|
+
overflow: hidden;
|
|
188
|
+
isolation: isolate;
|
|
189
|
+
background:
|
|
190
|
+
radial-gradient(ellipse 80% 60% at 15% 20%, rgba(99, 91, 255, 0.18) 0%, transparent 60%),
|
|
191
|
+
radial-gradient(ellipse 60% 80% at 85% 30%, rgba(0, 212, 255, 0.15) 0%, transparent 55%),
|
|
192
|
+
radial-gradient(ellipse 70% 50% at 50% 100%, rgba(255, 87, 209, 0.12) 0%, transparent 60%),
|
|
193
|
+
linear-gradient(180deg, #f6f9fc 0%, #ffffff 100%);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
hgroup::before {
|
|
197
|
+
content: '';
|
|
198
|
+
position: absolute;
|
|
199
|
+
inset: 0;
|
|
200
|
+
background-image:
|
|
201
|
+
linear-gradient(rgba(10, 37, 64, 0.04) 1px, transparent 1px),
|
|
202
|
+
linear-gradient(90deg, rgba(10, 37, 64, 0.04) 1px, transparent 1px);
|
|
203
|
+
background-size: 56px 56px;
|
|
204
|
+
mask-image: radial-gradient(ellipse 70% 60% at 50% 40%, black 0%, transparent 75%);
|
|
205
|
+
-webkit-mask-image: radial-gradient(ellipse 70% 60% at 50% 40%, black 0%, transparent 75%);
|
|
206
|
+
z-index: -1;
|
|
207
|
+
pointer-events: none;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
hgroup::after {
|
|
211
|
+
content: '';
|
|
212
|
+
position: absolute;
|
|
213
|
+
left: 0;
|
|
214
|
+
right: 0;
|
|
215
|
+
bottom: -1px;
|
|
216
|
+
height: 80px;
|
|
217
|
+
background: linear-gradient(180deg, transparent 0%, #ffffff 100%);
|
|
218
|
+
pointer-events: none;
|
|
219
|
+
z-index: -1;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
hgroup > * {
|
|
223
|
+
position: relative;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
hgroup > p:first-child {
|
|
227
|
+
display: inline-flex;
|
|
228
|
+
margin-bottom: 2rem;
|
|
229
|
+
padding: 0;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
hgroup > p:first-child small {
|
|
233
|
+
display: inline-flex;
|
|
234
|
+
align-items: center;
|
|
235
|
+
gap: 0.5rem;
|
|
236
|
+
background: rgba(255, 255, 255, 0.8);
|
|
237
|
+
backdrop-filter: blur(8px);
|
|
238
|
+
border: 1px solid var(--color-border);
|
|
239
|
+
padding: 0.4rem 0.9rem;
|
|
240
|
+
border-radius: 999px;
|
|
241
|
+
color: var(--color-text-soft);
|
|
242
|
+
font-size: 0.8rem;
|
|
243
|
+
font-weight: 500;
|
|
244
|
+
box-shadow: var(--shadow-sm);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
hgroup > p:first-child small::before {
|
|
248
|
+
content: '';
|
|
249
|
+
width: 6px;
|
|
250
|
+
height: 6px;
|
|
251
|
+
border-radius: 50%;
|
|
252
|
+
background: #00d24a;
|
|
253
|
+
box-shadow: 0 0 0 3px rgba(0, 210, 74, 0.2);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
hgroup h1 {
|
|
257
|
+
font-size: clamp(2.5rem, 6.5vw, 5rem);
|
|
258
|
+
font-weight: 800;
|
|
259
|
+
letter-spacing: -0.04em;
|
|
260
|
+
line-height: 1.05;
|
|
261
|
+
margin: 0 0 1.5rem;
|
|
262
|
+
max-width: 900px;
|
|
263
|
+
background: linear-gradient(135deg, #0a2540 0%, #1a1a4e 40%, #635bff 100%);
|
|
264
|
+
-webkit-background-clip: text;
|
|
265
|
+
background-clip: text;
|
|
266
|
+
color: transparent;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
hgroup h1 br { display: block; }
|
|
270
|
+
|
|
271
|
+
hgroup p {
|
|
272
|
+
font-size: 1.2rem;
|
|
273
|
+
line-height: 1.55;
|
|
274
|
+
color: var(--color-text-soft);
|
|
275
|
+
max-width: 640px;
|
|
276
|
+
margin: 0 0 2rem;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
hgroup > p:last-child {
|
|
280
|
+
display: flex;
|
|
281
|
+
gap: 0.75rem;
|
|
282
|
+
flex-wrap: wrap;
|
|
283
|
+
margin-top: 2.5rem;
|
|
284
|
+
margin-bottom: 0;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
hgroup > p:last-child a {
|
|
288
|
+
display: inline-flex;
|
|
289
|
+
align-items: center;
|
|
290
|
+
padding: 0.85rem 1.5rem;
|
|
291
|
+
border-radius: 999px;
|
|
292
|
+
font-weight: 600;
|
|
293
|
+
font-size: 0.95rem;
|
|
294
|
+
text-decoration: none;
|
|
295
|
+
transition: all var(--transition);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
hgroup > p:last-child a:first-child {
|
|
299
|
+
background: var(--color-text);
|
|
300
|
+
color: white;
|
|
301
|
+
box-shadow: 0 4px 14px rgba(10, 37, 64, 0.25);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
hgroup > p:last-child a:first-child:hover {
|
|
305
|
+
background: var(--color-accent);
|
|
306
|
+
transform: translateY(-1px);
|
|
307
|
+
box-shadow: 0 6px 20px rgba(99, 91, 255, 0.35);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
hgroup > p:last-child a:last-child {
|
|
311
|
+
background: rgba(255, 255, 255, 0.7);
|
|
312
|
+
backdrop-filter: blur(8px);
|
|
313
|
+
color: var(--color-text);
|
|
314
|
+
border: 1px solid var(--color-border);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
hgroup > p:last-child a:last-child:hover {
|
|
318
|
+
background: white;
|
|
319
|
+
border-color: var(--color-border-strong);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
@media (max-width: 640px) {
|
|
323
|
+
hgroup { padding: 4rem 1.25rem 5rem; }
|
|
324
|
+
hgroup h1 br { display: none; }
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/* ---------- SECTIONS ---------- */
|
|
328
|
+
|
|
329
|
+
main > section {
|
|
330
|
+
margin-bottom: 3rem;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
main > section > p:first-of-type {
|
|
334
|
+
font-size: 1.05rem;
|
|
335
|
+
max-width: var(--container-prose);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/* ---------- TRAINER CARDS (article in section) ---------- */
|
|
339
|
+
|
|
340
|
+
main > section > article {
|
|
341
|
+
background: var(--color-bg);
|
|
342
|
+
border: 1px solid var(--color-border);
|
|
343
|
+
border-radius: var(--radius);
|
|
344
|
+
padding: 1.75rem 2rem;
|
|
345
|
+
margin: 1rem 0;
|
|
346
|
+
box-shadow: var(--shadow-sm);
|
|
347
|
+
transition: all var(--transition);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
main > section > article:hover {
|
|
351
|
+
border-color: var(--color-border-strong);
|
|
352
|
+
box-shadow: var(--shadow-md);
|
|
353
|
+
transform: translateY(-2px);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
main > section > article header {
|
|
357
|
+
margin-bottom: 0.75rem;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
main > section > article h2 {
|
|
361
|
+
margin: 0 0 0.25rem;
|
|
362
|
+
font-size: 1.35rem;
|
|
363
|
+
font-weight: 700;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
main > section > article h2 a {
|
|
367
|
+
color: var(--color-text);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
main > section > article h2 a:hover {
|
|
371
|
+
color: var(--color-accent);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
main > section > article header p {
|
|
375
|
+
margin: 0;
|
|
376
|
+
font-size: 0.9rem;
|
|
377
|
+
color: var(--color-text-muted);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
main > section > article > p {
|
|
381
|
+
margin: 0.75rem 0 0.5rem;
|
|
382
|
+
color: var(--color-text-soft);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
main > section > article small {
|
|
386
|
+
display: inline-block;
|
|
387
|
+
background: var(--color-accent-soft);
|
|
388
|
+
color: var(--color-accent-hover);
|
|
389
|
+
padding: 0.25rem 0.75rem;
|
|
390
|
+
border-radius: 999px;
|
|
391
|
+
font-size: 0.8rem;
|
|
392
|
+
font-weight: 500;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/* ---------- TRAINER ARTICLE (single page) ---------- */
|
|
396
|
+
|
|
397
|
+
main > article {
|
|
398
|
+
max-width: var(--container-prose);
|
|
399
|
+
margin: 0 auto;
|
|
400
|
+
background: var(--color-bg);
|
|
401
|
+
border: 1px solid var(--color-border);
|
|
402
|
+
border-radius: var(--radius-lg);
|
|
403
|
+
padding: 3rem;
|
|
404
|
+
box-shadow: var(--shadow-md);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
main > article > header {
|
|
408
|
+
border-bottom: 1px solid var(--color-border);
|
|
409
|
+
padding-bottom: 1.5rem;
|
|
410
|
+
margin-bottom: 2rem;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
main > article > header h1 {
|
|
414
|
+
margin-bottom: 0.5rem;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
main > article > header p {
|
|
418
|
+
font-size: 1.1rem;
|
|
419
|
+
color: var(--color-text-muted);
|
|
420
|
+
margin: 0;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
main > article address {
|
|
424
|
+
font-style: normal;
|
|
425
|
+
display: inline-block;
|
|
426
|
+
background: var(--color-bg-soft);
|
|
427
|
+
padding: 0.75rem 1.25rem;
|
|
428
|
+
border-radius: var(--radius);
|
|
429
|
+
margin-bottom: 2rem;
|
|
430
|
+
color: var(--color-text-soft);
|
|
431
|
+
border: 1px solid var(--color-border);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
main > article section {
|
|
435
|
+
margin: 2rem 0;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
main > article section h2 {
|
|
439
|
+
font-size: 1.1rem;
|
|
440
|
+
font-weight: 600;
|
|
441
|
+
text-transform: uppercase;
|
|
442
|
+
letter-spacing: 0.05em;
|
|
443
|
+
color: var(--color-text-muted);
|
|
444
|
+
margin-top: 0;
|
|
445
|
+
margin-bottom: 1rem;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
main > article ul {
|
|
449
|
+
list-style: none;
|
|
450
|
+
padding: 0;
|
|
451
|
+
display: flex;
|
|
452
|
+
flex-wrap: wrap;
|
|
453
|
+
gap: 0.5rem;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
main > article ul li {
|
|
457
|
+
background: var(--color-accent-soft);
|
|
458
|
+
color: var(--color-accent-hover);
|
|
459
|
+
padding: 0.4rem 0.9rem;
|
|
460
|
+
border-radius: 999px;
|
|
461
|
+
font-size: 0.875rem;
|
|
462
|
+
font-weight: 500;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
main > article dl {
|
|
466
|
+
display: grid;
|
|
467
|
+
grid-template-columns: max-content 1fr;
|
|
468
|
+
gap: 0.75rem 1.5rem;
|
|
469
|
+
margin: 0;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
main > article dt {
|
|
473
|
+
color: var(--color-text-muted);
|
|
474
|
+
font-size: 0.875rem;
|
|
475
|
+
font-weight: 500;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
main > article dd {
|
|
479
|
+
margin: 0;
|
|
480
|
+
color: var(--color-text);
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
main > article > footer {
|
|
484
|
+
margin-top: 2.5rem;
|
|
485
|
+
padding-top: 1.5rem;
|
|
486
|
+
border-top: 1px solid var(--color-border);
|
|
487
|
+
text-align: center;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
/* ---------- RESPONSIVE ---------- */
|
|
491
|
+
|
|
492
|
+
@media (max-width: 640px) {
|
|
493
|
+
main { padding: 2rem 1rem 3rem; }
|
|
494
|
+
hgroup { padding: 2.5rem 1rem 3rem; margin: -2rem -1rem 2rem; }
|
|
495
|
+
main > article { padding: 1.5rem; }
|
|
496
|
+
main > article dl { grid-template-columns: 1fr; gap: 0.25rem 0; }
|
|
497
|
+
main > article dt { margin-top: 0.75rem; }
|
|
498
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Profile } from '../schema.ts';
|
|
2
|
+
|
|
3
|
+
export function esc(s: string | undefined | null): string {
|
|
4
|
+
if (!s) return '';
|
|
5
|
+
return s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function fullName(p: Profile): string {
|
|
9
|
+
return `${p.firstName} ${p.lastName}`;
|
|
10
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { Profile } from '../schema.ts';
|
|
2
|
+
import { esc, fullName } from './helpers.ts';
|
|
3
|
+
|
|
4
|
+
export function hero(p: Profile): string {
|
|
5
|
+
const name = fullName(p);
|
|
6
|
+
|
|
7
|
+
const badge = p.verified
|
|
8
|
+
? `<p><small>Profil zweryfikowany · ${esc(p.city ?? 'Polska')}</small></p>`
|
|
9
|
+
: '';
|
|
10
|
+
|
|
11
|
+
const ctas: string[] = [];
|
|
12
|
+
if (p.email) ctas.push(`<a href="mailto:${esc(p.email)}">Napisz wiadomość</a>`);
|
|
13
|
+
if (p.phone) ctas.push(`<a href="tel:${esc(p.phone.replace(/\s/g, ''))}">Zadzwoń</a>`);
|
|
14
|
+
const ctaBlock = ctas.length ? `<p>${ctas.join('\n')}</p>` : '';
|
|
15
|
+
|
|
16
|
+
return `<hgroup>
|
|
17
|
+
${badge}
|
|
18
|
+
<h1>${esc(name)}</h1>
|
|
19
|
+
<p>${esc(p.tagline ?? p.jobTitle)}</p>
|
|
20
|
+
${ctaBlock}
|
|
21
|
+
</hgroup>`;
|
|
22
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { esc } from './helpers.ts';
|
|
2
|
+
|
|
3
|
+
export interface LayoutProps {
|
|
4
|
+
title: string;
|
|
5
|
+
description?: string;
|
|
6
|
+
jsonLd?: object;
|
|
7
|
+
siteName?: string;
|
|
8
|
+
homeHref?: string;
|
|
9
|
+
baseUrl?: string;
|
|
10
|
+
headExtra?: string;
|
|
11
|
+
navExtra?: string;
|
|
12
|
+
footerContent?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const THEME_VERSION = '0.3.0';
|
|
16
|
+
|
|
17
|
+
export function layout(props: LayoutProps, body: string): string {
|
|
18
|
+
const {
|
|
19
|
+
title,
|
|
20
|
+
description,
|
|
21
|
+
jsonLd,
|
|
22
|
+
siteName = 'press2ai',
|
|
23
|
+
homeHref = '/',
|
|
24
|
+
baseUrl,
|
|
25
|
+
headExtra = '',
|
|
26
|
+
navExtra = '',
|
|
27
|
+
footerContent,
|
|
28
|
+
} = props;
|
|
29
|
+
|
|
30
|
+
const base = baseUrl ? baseUrl.replace(/\/$/, '') : '';
|
|
31
|
+
|
|
32
|
+
const footer = footerContent ??
|
|
33
|
+
`Powered by <a href="https://github.com/press2ai">press2ai</a> ·
|
|
34
|
+
<a href="https://www.npmjs.com/package/@press2ai/theme-specialist-glossy/v/${THEME_VERSION}">@press2ai/theme-specialist-glossy v${THEME_VERSION}</a>`;
|
|
35
|
+
|
|
36
|
+
return `<!doctype html>
|
|
37
|
+
<html lang="pl">
|
|
38
|
+
<head>
|
|
39
|
+
<meta charset="UTF-8">
|
|
40
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
41
|
+
<title>${esc(title)}</title>
|
|
42
|
+
${description ? `<meta name="description" content="${esc(description)}">` : ''}
|
|
43
|
+
<meta property="og:title" content="${esc(title)}">
|
|
44
|
+
${description ? `<meta property="og:description" content="${esc(description)}">` : ''}
|
|
45
|
+
${jsonLd ? `<script type="application/ld+json">${JSON.stringify(jsonLd).replace(/</g, '\\u003c')}</script>` : ''}
|
|
46
|
+
${base ? `<link rel="alternate" type="application/json" href="${esc(base)}/profile.json" title="Profile data (JSON)">
|
|
47
|
+
<link rel="alternate" type="application/schema+json" href="${esc(base)}/schema.json" title="JSON Schema">
|
|
48
|
+
<link rel="alternate" type="text/plain" href="${esc(base)}/llms.txt" title="LLM pointer">
|
|
49
|
+
<link rel="describedby" href="${esc(base)}/.well-known/ai-profile.json" type="application/json">` : ''}
|
|
50
|
+
${headExtra}
|
|
51
|
+
</head>
|
|
52
|
+
<body>
|
|
53
|
+
<header>
|
|
54
|
+
<nav>
|
|
55
|
+
<a href="${esc(homeHref)}"><strong>${esc(siteName)}</strong></a>
|
|
56
|
+
${navExtra}
|
|
57
|
+
</nav>
|
|
58
|
+
</header>
|
|
59
|
+
<main>${body}</main>
|
|
60
|
+
<footer>
|
|
61
|
+
<small>${footer}</small>
|
|
62
|
+
</footer>
|
|
63
|
+
</body>
|
|
64
|
+
</html>`;
|
|
65
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import type { Profile } from '../schema.ts';
|
|
2
|
+
import { esc, fullName } from './helpers.ts';
|
|
3
|
+
|
|
4
|
+
export function profileArticle(p: Profile): string {
|
|
5
|
+
const name = fullName(p);
|
|
6
|
+
const socials = Object.entries(p.social ?? {}).filter(([, v]) => v);
|
|
7
|
+
|
|
8
|
+
const verified = p.verified ? '<p><small>✓ Profil zweryfikowany</small></p>' : '';
|
|
9
|
+
|
|
10
|
+
const address = p.city
|
|
11
|
+
? `<address itemprop="address" itemscope itemtype="https://schema.org/PostalAddress">
|
|
12
|
+
<strong>Miasto: </strong>
|
|
13
|
+
<span itemprop="addressLocality">${esc(p.city)}</span>,
|
|
14
|
+
<span itemprop="addressCountry">${esc(p.country)}</span>
|
|
15
|
+
</address>`
|
|
16
|
+
: '';
|
|
17
|
+
|
|
18
|
+
const bio = p.bio
|
|
19
|
+
? `<section>
|
|
20
|
+
<h2>O mnie</h2>
|
|
21
|
+
<p itemprop="description">${esc(p.bio)}</p>
|
|
22
|
+
</section>`
|
|
23
|
+
: '';
|
|
24
|
+
|
|
25
|
+
const specialties = p.specialties.length > 0
|
|
26
|
+
? `<section>
|
|
27
|
+
<h2>Specjalizacje</h2>
|
|
28
|
+
<ul>
|
|
29
|
+
${p.specialties.map(s => `<li itemprop="knowsAbout">${esc(s)}</li>`).join('\n')}
|
|
30
|
+
</ul>
|
|
31
|
+
</section>`
|
|
32
|
+
: '';
|
|
33
|
+
|
|
34
|
+
const languages = p.languages.length > 0
|
|
35
|
+
? `<section>
|
|
36
|
+
<h2>Języki</h2>
|
|
37
|
+
<ul>
|
|
38
|
+
${p.languages.map(l => `<li itemprop="knowsLanguage">${esc(l)}</li>`).join('\n')}
|
|
39
|
+
</ul>
|
|
40
|
+
</section>`
|
|
41
|
+
: '';
|
|
42
|
+
|
|
43
|
+
const contactItems: string[] = [];
|
|
44
|
+
if (p.phone) contactItems.push(`<dt>Telefon</dt><dd><a href="tel:${esc(p.phone.replace(/\s/g, ''))}" itemprop="telephone">${esc(p.phone)}</a></dd>`);
|
|
45
|
+
if (p.email) contactItems.push(`<dt>E-mail</dt><dd><a href="mailto:${esc(p.email)}" itemprop="email">${esc(p.email)}</a></dd>`);
|
|
46
|
+
if (p.website) contactItems.push(`<dt>Strona WWW</dt><dd><a href="${esc(p.website)}" itemprop="url" rel="noopener">${esc(p.website)}</a></dd>`);
|
|
47
|
+
const socialUrlPrefix: Record<string, string> = {
|
|
48
|
+
linkedin: 'https://linkedin.com/in/',
|
|
49
|
+
youtube: 'https://youtube.com/@',
|
|
50
|
+
tiktok: 'https://tiktok.com/@',
|
|
51
|
+
facebook: 'https://facebook.com/',
|
|
52
|
+
twitter: 'https://x.com/',
|
|
53
|
+
instagram: 'https://instagram.com/',
|
|
54
|
+
};
|
|
55
|
+
for (const [name, handle] of socials) {
|
|
56
|
+
const prefix = socialUrlPrefix[name] ?? `https://${esc(name)}.com/`;
|
|
57
|
+
contactItems.push(`<dt>${esc(name)}</dt><dd><a href="${prefix}${esc(handle as string)}" rel="noopener">@${esc(handle as string)}</a></dd>`);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const contact = contactItems.length > 0
|
|
61
|
+
? `<section>
|
|
62
|
+
<h2>Kontakt</h2>
|
|
63
|
+
<dl>
|
|
64
|
+
${contactItems.join('\n')}
|
|
65
|
+
</dl>
|
|
66
|
+
</section>`
|
|
67
|
+
: '';
|
|
68
|
+
|
|
69
|
+
const business = p.business?.name
|
|
70
|
+
? `<section>
|
|
71
|
+
<h2>Dane firmy</h2>
|
|
72
|
+
<dl>
|
|
73
|
+
<dt>Nazwa</dt><dd>${esc(p.business.name)}</dd>
|
|
74
|
+
${p.business.taxId ? `<dt>NIP</dt><dd>${esc(p.business.taxId)}</dd>` : ''}
|
|
75
|
+
${p.business.registryId ? `<dt>REGON</dt><dd>${esc(p.business.registryId)}</dd>` : ''}
|
|
76
|
+
${(p.business.classification?.length ?? 0) > 0 ? `<dt>PKD</dt><dd>${p.business.classification!.map(c => esc(c)).join(', ')}</dd>` : ''}
|
|
77
|
+
</dl>
|
|
78
|
+
</section>`
|
|
79
|
+
: '';
|
|
80
|
+
|
|
81
|
+
return `<article itemscope itemtype="https://schema.org/Person">
|
|
82
|
+
<header>
|
|
83
|
+
<h1 itemprop="name">${esc(name)}</h1>
|
|
84
|
+
<p itemprop="jobTitle">${esc(p.jobTitle)}</p>
|
|
85
|
+
${verified}
|
|
86
|
+
</header>
|
|
87
|
+
${address}
|
|
88
|
+
${bio}
|
|
89
|
+
${specialties}
|
|
90
|
+
${languages}
|
|
91
|
+
${contact}
|
|
92
|
+
${business}
|
|
93
|
+
</article>`;
|
|
94
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { Profile } from '../schema.ts';
|
|
2
|
+
import { esc, fullName } from './helpers.ts';
|
|
3
|
+
|
|
4
|
+
export function profileCard(p: Profile, href: string): string {
|
|
5
|
+
const name = fullName(p);
|
|
6
|
+
|
|
7
|
+
const city = p.city ? ` · <span itemprop="address" itemscope itemtype="https://schema.org/PostalAddress"><span itemprop="addressLocality">${esc(p.city)}</span></span>` : '';
|
|
8
|
+
|
|
9
|
+
const bio = p.bio ? `<p itemprop="description">${esc(p.bio)}</p>` : '';
|
|
10
|
+
|
|
11
|
+
const specs = p.specialties.length > 0
|
|
12
|
+
? `<p><small>${p.specialties.map(s => esc(s)).join(' · ')}</small></p>`
|
|
13
|
+
: '';
|
|
14
|
+
|
|
15
|
+
return `<article itemscope itemtype="https://schema.org/Person">
|
|
16
|
+
<header>
|
|
17
|
+
<h2><a href="${esc(href)}" itemprop="url"><span itemprop="name">${esc(name)}</span></a></h2>
|
|
18
|
+
<p><span itemprop="jobTitle">${esc(p.jobTitle)}</span>${city}</p>
|
|
19
|
+
</header>
|
|
20
|
+
${bio}
|
|
21
|
+
${specs}
|
|
22
|
+
</article>`;
|
|
23
|
+
}
|