0
mirror of https://github.com/valentineus/popov.link.git synced 2025-07-19 22:58:48 +03:00

Compare commits

...

26 Commits

Author SHA1 Message Date
renovate[bot]
ae3176da79 fix(deps): update dependency astro to v5.12.0 (#68)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-17 22:28:10 +00:00
renovate[bot]
3bb4d50767 fix(deps): update dependency astro to v5.11.2 (#67)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-16 19:34:50 +00:00
renovate[bot]
dd33ec8e48 fix(deps): update all digest updates to v7.1.0 (#66)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-15 04:08:19 +00:00
renovate[bot]
4698e50cb7 fix(deps): update dependency astro to v5.11.1 (#65)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-14 16:47:09 +00:00
renovate[bot]
05a0579a91 chore(deps): update mcr.microsoft.com/devcontainers/javascript-node docker tag to v24 (#63) 2025-07-10 21:34:30 +04:00
renovate[bot]
80fcd46280 fix(deps): update dependency sharp to v0.34.3 (#62)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-10 16:29:23 +00:00
renovate[bot]
7634a2e325 fix(deps): update dependency astro to v5.11.0 (#61)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-03 16:35:07 +00:00
renovate[bot]
29385b1edf fix(deps): update dependency astro to v5.10.2 (#60)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-01 16:30:49 +00:00
renovate[bot]
ae7dac3099 chore(deps): update dependency prettier to v3.6.2 (#59)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-27 05:23:58 +00:00
renovate[bot]
d8a59ef4c4 chore(deps): update dependency prettier to v3.6.1 (#58)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-25 19:13:58 +00:00
renovate[bot]
4412049beb fix(deps): update dependency astro to v5.10.1 (#57)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-23 19:11:55 +00:00
renovate[bot]
c32aa4e773 chore(deps): update dependency prettier to v3.6.0 (#56)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-23 07:43:05 +00:00
renovate[bot]
c9d67d9210 fix(deps): update dependency astro to v5.10.0 (#55)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-19 19:52:42 +00:00
renovate[bot]
d99c901b42 fix(deps): update dependency astro to v5.9.4 (#54)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-17 17:06:39 +00:00
15fdeb46f1 chore: update README for consistency
- Removed duplicate line regarding project maintenance to streamline the README content.
2025-06-15 12:22:29 +00:00
15795a5337 chore: update GitHub Actions workflow for repository mirroring
- Modified the workflow trigger to include both push and pull request events on the master branch.
- Set permissions for the workflow to allow read access to contents.
2025-06-15 12:02:26 +00:00
bc4f65c1f6 chore: update README and add GitHub Actions workflow for repository mirroring
- Updated the README to change the read-only mirror link to the new URL.
- Added a GitHub Actions workflow for mirroring the repository to a remote Git repository.
2025-06-15 11:51:56 +00:00
a81117972d feat: implement Open Graph image generation and enhance configuration
- Added ogImages integration to generate Open Graph images for blog posts.
- Updated configuration to include Open Graph settings and default preview image.
- Refactored Head component to utilize new preview property for Open Graph meta tags.
- Enhanced blog post schema to include preview image for structured data representation.
- Introduced utility functions for creating Open Graph images with dynamic content.
2025-06-14 19:25:16 +00:00
3d0f485746 feat: add Open Graph and JSON-LD support to Head component
- Introduced OpenGraph component for enhanced social media sharing with Open Graph meta tags.
- Updated Head component to include OpenGraph and JSON-LD for improved SEO and structured data representation.
- Added comments for better clarity on meta tags and JSON-LD integration.
2025-06-14 12:19:01 +00:00
25ebd94466 refactor: remove preview property from blog post schema and configuration
- Eliminated the optional `preview` field from the blog post schema and configuration to streamline data handling.
- Updated the blog post schema to utilize the default image directly from configuration, enhancing consistency in image representation.
2025-06-14 12:12:16 +00:00
0473060773 feat: add configuration and default image for blog posts
- Introduced a new configuration file to centralize author information and default image settings for blog posts.
- Added a default image path in the blog post configuration for improved content presentation.
- Updated blog post schema to utilize the new configuration for author details, enhancing structured data representation.
2025-06-14 12:08:48 +00:00
a65e9c8455 feat: enhance blog post and page schema with new properties
- Added optional `basedOn` field to blog post schema for better content attribution.
- Updated blog post markdown to include `basedOn` reference for improved context.
- Refactored page schema to replace deprecated website schema, enhancing structured data representation.
- Adjusted 404 and index pages to utilize the new page schema for consistency and SEO improvements.
2025-06-14 11:47:17 +00:00
17f9a467d7 refactor: update blog post date handling and schema
- Replaced `pubDate` with `datePublished` in blog post components for consistency.
- Updated sorting logic in blog sections to use `datePublished`.
- Enhanced blog post schema to include `dateModified` for better structured data representation.
- Adjusted various blog markdown files to reflect the new date fields.
2025-06-14 11:25:17 +00:00
3df02c5304 feat: enhance blog post schema and structure
- Added description and lang parameters to the blogPostSchema for improved structured data.
- Updated the blog post layout to include a header section for better semantic structure and accessibility.
2025-06-14 11:09:34 +00:00
9777d996d1 refactor: enhance PostElement structure and update blog schema
- Wrapped the post link in an <article> tag for improved semantic structure.
- Updated blogSchema to include posts for better structured data representation.
- Adjusted the blog index to utilize the new posts parameter for enhanced SEO.
2025-06-14 11:01:42 +00:00
1c15151ef5 fix: enhance accessibility by adding role attribute to SVG icons 2025-06-14 10:53:20 +00:00
68 changed files with 1924 additions and 323 deletions

View File

@@ -1,5 +1,5 @@
{
"image": "mcr.microsoft.com/devcontainers/javascript-node:22",
"image": "mcr.microsoft.com/devcontainers/javascript-node:24",
"forwardPorts": [4321],
"portsAttributes": {
"4321": {

25
.github/workflows/mirror.yml vendored Normal file
View File

@@ -0,0 +1,25 @@
name: Mirror
on:
push:
branches: [master]
pull_request:
branches: [master]
permissions:
contents: read
jobs:
mirror:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Mirror to remote repository
uses: yesolutions/mirror-action@master
with:
REMOTE: "https://git.popov.link/popov.link.git"
GIT_USERNAME: ${{ secrets.GIT_USERNAME }}
GIT_PASSWORD: ${{ secrets.GIT_PASSWORD }}

View File

@@ -41,9 +41,9 @@ npm run preview
## Project Info
- Maintained by [Valentin Popov](mailto:valentin@popov.link)
- Issues: [GitHub](https://github.com/valentineus/popov.link/issues)
- Read-only mirror: [code.popov.link](https://code.popov.link/valentineus/popov.link)
- Read-only mirror: [git.popov.link](https://git.popov.link/popov.link/)
- Maintained by [Valentin Popov](mailto:valentin@popov.link)
## Comments

View File

@@ -1,11 +1,12 @@
import { defineConfig } from "astro/config";
import { remarkReadingTime } from "./src/plugins/remarkReadingTime";
import ogImages from "./src/integrations/ogImages";
import sitemap from "@astrojs/sitemap";
export default defineConfig({
site: "https://popov.link",
output: "static",
integrations: [sitemap()],
integrations: [sitemap(), ogImages()],
build: {
inlineStylesheets: "always",
},

1809
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -23,15 +23,22 @@
"@astrojs/check": "^0.9.4",
"@astrojs/rss": "^4.0.12",
"@astrojs/sitemap": "^3.4.1",
"@resvg/resvg-js": "^2.6.2",
"astro": "^5.9.0",
"autoprefixer": "^10.4.21",
"cssnano": "^7.0.7",
"cssnano-preset-advanced": "^7.0.7",
"dayjs": "^1.11.13",
"geist": "^1.4.2",
"globby": "^14.1.0",
"gray-matter": "^4.0.3",
"mdast-util-to-string": "^4.0.0",
"reading-time": "^1.5.0",
"sass": "^1.89.1",
"satori": "^0.15.2",
"satori-html": "^0.3.2",
"schema-dts": "^1.1.5",
"sharp": "^0.34.2",
"typescript": "^5"
},
"devDependencies": {

BIN
public/images/photo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 389 KiB

2
public/images/preview/.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
*
!.gitignore

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -4,15 +4,19 @@ import JsonLd from "./JsonLd.astro";
type Props = {
readonly description: string;
readonly title: string;
readonly preview: string;
readonly schema: WithContext<Thing>;
readonly title: string;
};
const canonicalURL = new URL(Astro.url.pathname, Astro.site);
const { description, title, schema } = Astro.props;
const { description, preview, schema, title } = Astro.props;
const canonicalUrl = new URL(Astro.url.pathname, Astro.site);
const previewUrl = new URL(preview, Astro.site);
---
<head>
<!-- Meta Tags -->
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta http-equiv="content-type" content="text/html; charset=utf-8" />
@@ -22,15 +26,29 @@ const { description, title, schema } = Astro.props;
<link href="/feed.xml" rel="alternate" title="RSS" type="application/atom+xml" />
<link href="/sitemap-index.xml" rel="sitemap" />
<link href={canonicalURL} rel="canonical" />
<link href={canonicalUrl} rel="canonical" />
<title>{title}</title>
<!-- Icons -->
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
<link rel="icon" type="image/png" href="/favicon.png" />
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
<link rel="manifest" href="/manifest.json" />
<meta name="theme-color" content="#ffffff" />
<!-- Open Graph -->
<meta property="og:type" content="website" />
<meta property="og:title" content={title} />
<meta property="og:description" content={description} />
<meta property="og:image" content={previewUrl} />
<meta property="og:url" content={canonicalUrl} />
<!-- Twitter Cards -->
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content={title} />
<meta name="twitter:description" content={description} />
<meta name="twitter:image" content={previewUrl} />
<JsonLd schema={schema} />
</head>

View File

@@ -13,7 +13,7 @@
</style>
<a href="mailto:valentin@popov.link" title="E-Mail" target="_blank">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-label="E-Mail">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-label="E-Mail" aria-hidden="true">
<path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"></path>
<polyline points="22,6 12,13 2,6"></polyline>
</svg>

View File

@@ -13,7 +13,7 @@
</style>
<a href="https://github.com/valentineus" title="GitHub" target="_blank">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-label="GitHub">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-label="GitHub" aria-hidden="true">
<path
d="M9 19c-5 1.5-5-2.5-7-3m14 6v-3.87a3.37 3.37 0 0 0-.94-2.61c3.14-.35 6.44-1.54 6.44-7A5.44 5.44 0 0 0 20 4.77 5.07 5.07 0 0 0 19.91 1S18.73.65 16 2.48a13.38 13.38 0 0 0-7 0C6.27.65 5.09 1 5.09 1A5.07 5.07 0 0 0 5 4.77a5.44 5.44 0 0 0-1.5 3.78c0 5.42 3.3 6.61 6.44 7A3.37 3.37 0 0 0 9 18.13V22"
>

View File

@@ -13,7 +13,7 @@
</style>
<a href="https://www.linkedin.com/in/valentineus/" title="LinkedIn" target="_blank">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-label="LinkedIn">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-label="LinkedIn" aria-hidden="true">
<path d="M16 8a6 6 0 0 1 6 6v7h-4v-7a2 2 0 0 0-2-2 2 2 0 0 0-2 2v7h-4v-7a6 6 0 0 1 6-6z"></path>
<rect x="2" y="9" width="4" height="12"></rect>
<circle cx="4" cy="4" r="2"></circle>

View File

@@ -9,7 +9,7 @@
</style>
<a href="/feed.xml" title="RSS Feed" target="_blank">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-label="RSS Feed">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-label="RSS Feed" aria-hidden="true">
<path d="M4 11a9 9 0 0 1 9 9"></path>
<path d="M4 4a16 16 0 0 1 16 16"></path>
<circle cx="5" cy="19" r="1"></circle>

View File

@@ -9,4 +9,5 @@ const { schema } = Astro.props;
const json = JSON.stringify(schema);
---
<!-- JSON-LD -->
<script is:inline type="application/ld+json" set:html={json} />

View File

@@ -8,7 +8,9 @@ type Props = {
const { post } = Astro.props;
const { remarkPluginFrontmatter } = await post.render();
const formattedDate = dayjs(post.data.pubDate.toString()).format("MMMM DD, YYYY");
const formattedDate = dayjs(post.data.datePublished.toString()).format("MMMM DD, YYYY");
const datePublished = post.data.datePublished.toISOString();
---
<style lang="scss">
@@ -25,12 +27,14 @@ const formattedDate = dayjs(post.data.pubDate.toString()).format("MMMM DD, YYYY"
</style>
<li>
<a href={`/blog/${post.slug}`} lang={post.data.lang}>{post.data.title}</a>
<div>
<small>
<time datetime={post.data.pubDate.toISOString()} lang="en">{formattedDate}</time>
<span>•</span>
<span>{remarkPluginFrontmatter.minutesRead}</span>
</small>
</div>
<article>
<a href={`/blog/${post.slug}`} lang={post.data.lang}>{post.data.title}</a>
<div>
<small>
<time datetime={datePublished} lang="en">{formattedDate}</time>
<span></span>
<span>{remarkPluginFrontmatter.minutesRead}</span>
</small>
</div>
</article>
</li>

View File

@@ -7,7 +7,7 @@ const posts = await getCollection("blog", ({ data }) => {
return data.draft !== true;
});
posts.sort((a, b) => b.data.pubDate.getTime() - a.data.pubDate.getTime());
posts.sort((a, b) => b.data.datePublished.getTime() - a.data.datePublished.getTime());
const latestPosts = posts.slice(0, 5);
---
@@ -32,8 +32,8 @@ const latestPosts = posts.slice(0, 5);
</a>
<small>
<time datetime={post.data.pubDate.toISOString()} lang="en">
{dayjs(post.data.pubDate.toString()).format("MMMM DD, YYYY")}
<time datetime={post.data.datePublished.toISOString()} lang="en">
{dayjs(post.data.datePublished.toString()).format("MMMM DD, YYYY")}
</time>
</small>
</li>

29
src/config.ts Normal file
View File

@@ -0,0 +1,29 @@
export const config = {
author: {
name: "Valentin Popov",
email: "valentin@popov.link",
url: "https://popov.link/",
sameAs: ["https://www.linkedin.com/in/valentineus/", "https://github.com/valentineus"],
},
// Open Graph
og: {
color: {
bg: "#181818",
bgCode: "#3b3d42",
blossom: "#6da13f",
text: "#dee2e6",
},
defaultPreview: "/images/photo.png",
dimensions: {
height: 630,
width: 1200,
},
fonts: {
bold: "./src/assets/JetBrainsMono/JetBrainsMono-Bold.ttf",
regular: "./src/assets/JetBrainsMono/JetBrainsMono-Regular.ttf",
},
photo: "./public/images/photo.png",
website: "popov.link",
},
};

View File

@@ -1,8 +1,9 @@
---
basedOn: "https://adrianhenke.wordpress.com/2008/12/05/create-lib-file-from-dll/"
title: 'Create ".lib" file from ".dll" (archive)'
author: "Adrian Henke"
pubDate: "2023-05-04"
description: "Learn how to generate a *.lib file from a *.dll with this comprehensive guide. Using the Visual Studio Command Prompt and Microsoft's recommended tools, this article walks you through the steps for a seamless process. Perfect for developers working with 3rd party win dll's."
datePublished: "2023-05-04"
dateModified: "2023-05-04"
lang: "en"
---

View File

@@ -1,8 +1,8 @@
---
title: "Горячая перезагрузка ElectronJS приложения"
author: "Valentin Popov"
pubDate: "2019-08-15"
description: "Руководство по автоматической перезагрузке приложений на Electron с помощью пакетов electron-reload и electron-webpack. Обход проблем с совместимостью и использование HMR для renderer процесса."
datePublished: "2019-08-15"
dateModified: "2019-08-15"
lang: "ru"
---

View File

@@ -1,8 +1,8 @@
---
title: "Example Content"
author: "Example User"
pubDate: "2018-01-01"
description: "Howdy! This is an example blog post that shows several types of HTML content supported in this theme."
datePublished: "2018-01-01"
dateModified: "2018-01-01"
lang: "en"
draft: true
---

View File

@@ -1,8 +1,8 @@
---
title: 'Получение исходного кода "Chromium Projects"'
author: "Valentin Popov"
pubDate: "2012-01-30"
description: "Изучение исходных кодов Chromium: подготовка системы и установка необходимых программных компонентов. Руководство для начинающих разработчиков. Получите инструкции по установке Microsoft Visual Studio, Cygwin, Python и других инструментов. Действительно на январь-февраль 2012 года."
datePublished: "2012-01-30"
dateModified: "2012-01-30"
lang: "ru"
---

View File

@@ -1,8 +1,8 @@
---
title: "Установка Moodle в Fedora"
author: "Valentin Popov"
pubDate: "2018-07-23"
description: "Решение проблем установки Moodle из-за SELinux: как настроить правила доступа для устранения ошибок в веб-интерфейсе и при работе с cURL. Практические советы и команды."
datePublished: "2018-07-23"
dateModified: "2018-07-23"
lang: "ru"
---

View File

@@ -1,8 +1,8 @@
---
title: "Компиляция Rust на TL-MR3020"
author: "Valentin Popov"
pubDate: "2023-05-01"
description: 'Как настроить и оптимизировать проект Rust для кросс-компиляции на TP-Link TL-MR3020 с использованием Fedora Linux 38 и OpenWrt 22.03.4. Шаг за шагом от базового "Hello, World!" до асинхронного TCP сервера.'
datePublished: "2023-05-01"
dateModified: "2023-05-01"
lang: "ru"
---

View File

@@ -3,12 +3,13 @@ import { defineCollection, z } from "astro:content";
const blog = defineCollection({
type: "content",
schema: z.object({
author: z.string(),
basedOn: z.optional(z.string()),
dateModified: z.coerce.date(),
datePublished: z.coerce.date(),
description: z.string(),
draft: z.optional(z.boolean()),
pubDate: z.coerce.date(),
title: z.string(),
lang: z.string(),
title: z.string(),
}),
});

View File

@@ -0,0 +1,47 @@
import type { AstroIntegration } from "astro";
import { createOgImage } from "../utils/createOgImage";
import { globby } from "globby";
import fs from "fs/promises";
import matter from "gray-matter";
import path from "path";
const postsDir = path.resolve("./src/content/blog");
const outDir = path.resolve("./public/images/preview");
export default function ogImageGenerator(): AstroIntegration {
return {
name: "og-images",
hooks: {
"astro:build:setup": async ({ logger }) => {
await fs.mkdir(outDir, { recursive: true });
const mdFiles = await globby("*.md", { cwd: postsDir });
logger.info(`${mdFiles.length} posts found`);
const results = await Promise.allSettled(
mdFiles.map(async (file) => {
const slug = file.replace(/\.md$/, "");
const content = await fs.readFile(path.join(postsDir, file), "utf-8");
const { data } = matter(content);
const png = await createOgImage(data.title, data.datePublished);
const outPath = path.join(outDir, `${slug}.png`);
await fs.writeFile(outPath, png);
logger.info(`OG image created: ${slug}`);
})
);
results.forEach((r) => {
if (r.status === "rejected") {
logger.error(`Error for ${r.reason.slug}: ${r.reason.message}`);
}
});
const failures = results.filter((r) => r.status === "rejected");
if (failures.length) {
throw new Error(`Failed to generate OG images for ${failures.length} posts`);
}
},
},
};
}

View File

@@ -7,16 +7,17 @@ import "../scss/global.scss";
type Props = {
readonly description: string;
readonly title: string;
readonly lang: string;
readonly preview: string;
readonly schema: WithContext<Thing>;
readonly title: string;
};
const { title, description, lang, schema } = Astro.props;
const { description, lang, preview, schema, title } = Astro.props;
---
<html lang={lang}>
<Head title={title} description={description} schema={schema} />
<Head title={title} description={description} preview={preview} schema={schema} />
<body>
<main>

View File

@@ -1,18 +1,23 @@
---
import { config } from "../config";
import Layout from "../layouts/BaseLayout.astro";
import websiteSchema from "../utils/schemas/websiteSchema";
import pageSchema from "../utils/schemas/pageSchema";
const title = "404 — Page Not Found | Valentin Popov";
const description = "The page you're looking for doesn't exist!";
const preview = config.og.defaultPreview;
const lang = "en";
const schema = websiteSchema({
const schema = pageSchema({
siteUrl: new URL("/", Astro.site).toString(),
page: "/404",
title,
description,
lang,
});
---
<Layout title={title} description={description} lang={lang} schema={schema}>
<Layout title={title} description={description} preview={preview} lang={lang} schema={schema}>
<div style={{ "text-align": "center" }}>
<h1>404</h1>
<p><strong>Page not found</strong></p>

View File

@@ -22,20 +22,27 @@ const post = Astro.props;
const { Content, remarkPluginFrontmatter } = await post.render();
const title = post.data.title;
const description = post.data.description;
const author = post.data.author;
const isBasedOn = post.data.basedOn;
const lang = post.data.lang;
const preview = `/images/preview/${post.slug}.png`;
const slug = post.slug;
const title = post.data.title;
const formattedData = dayjs(post.data.pubDate.toString()).format("MMMM DD, YYYY");
const data = post.data.pubDate.toISOString();
const dateModified = post.data.dateModified?.toISOString();
const datePublished = post.data.datePublished.toISOString();
const formattedDate = dayjs(post.data.datePublished.toString()).format("MMMM DD, YYYY");
const schema = blogPostSchema({
siteUrl: new URL("/", Astro.site).toString(),
dateModified,
datePublished,
description,
isBasedOn,
lang,
preview,
slug,
title,
slug: post.slug,
datePublished: data,
author,
});
---
@@ -47,23 +54,20 @@ const schema = blogPostSchema({
}
</style>
<Layout title={title} description={description} lang={lang} schema={schema}>
<Layout title={title} description={description} preview={preview} lang={lang} schema={schema}>
<article>
<section>
<header>
<h1>{title}</h1>
</section>
<section>
<p>
<small>
Posted
<time datetime={data} lang="en">{formattedData}</time>
by&nbsp;{author}
<time datetime={datePublished} lang="en">{formattedDate}</time>
<span>&nbsp;•&nbsp;</span>
<span>{remarkPluginFrontmatter.minutesRead}</span>
</small>
</p>
</section>
</header>
<section>
<Content />

View File

@@ -1,19 +1,20 @@
---
import type { CollectionEntry } from "astro:content";
import { config } from "../../config";
import { getCollection } from "astro:content";
import blogSchema from "../../utils/schemas/blogSchema";
import Layout from "../../layouts/BaseLayout.astro";
import PostElement from "../../components/PostElement.astro";
import blogSchema from "../../utils/schemas/blogSchema";
import RSSIcon from "../../components/Icons/RSS.astro";
const posts = await getCollection("blog", ({ data }) => {
return data.draft !== true;
});
posts.sort((a, b) => b.data.pubDate.getTime() - a.data.pubDate.getTime());
posts.sort((a, b) => b.data.datePublished.getTime() - a.data.datePublished.getTime());
const postsByYear = posts.reduce<Record<string, CollectionEntry<"blog">[]>>((acc, post) => {
const year = post.data.pubDate.getFullYear().toString();
const year = post.data.datePublished.getFullYear().toString();
if (!acc[year]) {
acc[year] = [];
}
@@ -25,15 +26,17 @@ const years = Object.keys(postsByYear).sort((a, b) => Number(b) - Number(a));
const title = "Valentin Popov's Blog | Software Development, Leadership & Open-Source";
const description = "Explore Valentin Popov's blog on software development, tech leadership, and open-source experiments. Stay updated with in-depth tutorials and expert insights.";
const preview = config.og.defaultPreview;
const lang = "en";
const schema = blogSchema({
siteUrl: new URL("/", Astro.site).toString(),
title,
posts,
});
---
<Layout title={title} description={description} lang={lang} schema={schema}>
<Layout title={title} description={description} preview={preview} lang={lang} schema={schema}>
<section>
<h1>
Blog posts

View File

@@ -1,21 +1,26 @@
---
import Layout from "../layouts/BaseLayout.astro";
import { config } from "../config";
import LatestPostsSection from "../components/Sections/LatestPosts.astro";
import Layout from "../layouts/BaseLayout.astro";
import pageSchema from "../utils/schemas/pageSchema";
import SocialLinksSection from "../components/Sections/SocialLinks.astro";
import WelcomeSection from "../components/Sections/Welcome.astro";
import websiteSchema from "../utils/schemas/websiteSchema";
const title = "Valentin Popov Software Developer & Team Lead | Tech Insights";
const description = "Blog by Valentin Popov — software developer and team lead writing about code, side projects, digital tools, and fun experiments.";
const preview = config.og.defaultPreview;
const lang = "en";
const schema = websiteSchema({
const schema = pageSchema({
siteUrl: new URL("/", Astro.site).toString(),
page: "/",
title,
description,
lang,
});
---
<Layout title={title} description={description} lang={lang} schema={schema}>
<Layout title={title} description={description} preview={preview} lang={lang} schema={schema}>
<WelcomeSection />
<SocialLinksSection />
<LatestPostsSection />

View File

@@ -0,0 +1,52 @@
import { config } from "../config";
import { html } from "satori-html";
import { resources } from "./ogResources";
import { Resvg } from "@resvg/resvg-js";
import dayjs from "dayjs";
import satori from "satori";
export async function createOgImage(title: string, datePublished: Date): Promise<Buffer> {
const formattedDate = dayjs(datePublished).format("MMMM DD, YYYY");
const markup = await satori(
html(`
<div tw="flex flex-col w-full h-full" style="background-color: ${config.og.color.bg}">
<div tw="flex flex-col w-full h-4/5 p-10 justify-center">
<div tw="text-2xl mb-6" style="color: ${config.og.color.text}">${formattedDate}</div>
<div tw="flex text-6xl w-full font-bold" style="color: ${config.og.color.text}">${title}</div>
</div>
<div tw="w-full h-1/5 flex p-10 items-center justify-between text-2xl" style="border-top: 1px solid ${config.og.color.bgCode}">
<div tw="flex items-center">
<span tw="ml-3" style="color: ${config.og.color.text}">${config.og.website.toLocaleUpperCase()}</span>
</div>
<div tw="flex items-center">
<img src="${resources.photoBase64}" tw="w-15 h-15 rounded-full" />
<div tw="flex flex-col ml-4">
<span style="color: ${config.og.color.text}">${config.author.name}</span>
<span style="color: ${config.og.color.blossom}">${config.author.email}</span>
</div>
</div>
</div>
</div>
`),
{
width: config.og.dimensions.width,
height: config.og.dimensions.height,
fonts: [
{
name: "Inter",
data: resources.fonts.regular,
weight: 400,
},
{
name: "Inter",
data: resources.fonts.bold,
weight: 700,
},
],
}
);
const image = new Resvg(markup, { fitTo: { mode: "width", value: config.og.dimensions.width } });
return image.render().asPng();
}

15
src/utils/ogResources.ts Normal file
View File

@@ -0,0 +1,15 @@
import { config } from "../config";
import fs from "fs/promises";
import path from "path";
import sharp from "sharp";
export const resources = {
fonts: {
regular: await fs.readFile(path.resolve(config.og.fonts.regular)),
bold: await fs.readFile(path.resolve(config.og.fonts.bold)),
},
photoBase64: await (async () => {
const buf = await fs.readFile(path.resolve(config.og.photo));
return "data:image/png;base64," + (await sharp(buf).resize(120, 120).png({ quality: 95 }).toBuffer()).toString("base64");
})(),
};

View File

@@ -1,21 +1,37 @@
import type { WithContext, BlogPosting } from "schema-dts";
import { config } from "../../config";
export type BlogPostSchemaParams = {
readonly siteUrl: string;
readonly title: string;
readonly slug: string;
readonly dateModified: string;
readonly datePublished: string;
readonly author: string;
readonly description: string;
readonly isBasedOn?: string;
readonly lang: string;
readonly preview: string;
readonly siteUrl: string;
readonly slug: string;
readonly title: string;
};
export default ({ siteUrl, title, slug, datePublished, author }: BlogPostSchemaParams): WithContext<BlogPosting> => ({
export default ({ siteUrl, slug, title, description, preview, datePublished, dateModified, lang, isBasedOn }: BlogPostSchemaParams): WithContext<BlogPosting> => ({
"@context": "https://schema.org",
"@type": "BlogPosting",
"url": new URL(`/blog/${slug}`, siteUrl).toString(),
"headline": title,
"description": description,
"image": new URL(preview, siteUrl).toString(),
"datePublished": datePublished,
"dateModified": dateModified,
"inLanguage": lang,
"author": {
"@type": "Person",
"name": author,
"name": config.author.name,
"url": config.author.url,
"sameAs": config.author.sameAs,
},
"mainEntityOfPage": {
"@type": "WebPage",
"@id": new URL(`/blog/${slug}`, siteUrl).toString(),
},
...(isBasedOn && { isBasedOn: isBasedOn }),
});

View File

@@ -1,13 +1,26 @@
import type { WithContext, Blog } from "schema-dts";
import type { WithContext, CollectionPage } from "schema-dts";
import type { CollectionEntry } from "astro:content";
export type BlogSchemaParams = {
readonly posts: CollectionEntry<"blog">[];
readonly siteUrl: string;
readonly title: string;
};
export default ({ siteUrl, title }: BlogSchemaParams): WithContext<Blog> => ({
export default ({ siteUrl, title, posts }: BlogSchemaParams): WithContext<CollectionPage> => ({
"@context": "https://schema.org",
"@type": "Blog",
"@type": "CollectionPage",
"url": new URL("/blog/", siteUrl).toString(),
"name": title,
"mainEntity": {
"@type": "ItemList",
"itemListOrder": "https://schema.org/ItemListOrderDescending",
"numberOfItems": posts.length,
"itemListElement": posts.map((post, index) => ({
"@type": "ListItem",
"position": index + 1,
"url": new URL(`/blog/${post.slug}`, siteUrl).toString(),
"name": post.data.title,
})),
},
});

View File

@@ -0,0 +1,23 @@
import type { WithContext, WebPage } from "schema-dts";
export type WebsiteSchemaParams = {
readonly description: string;
readonly page: string;
readonly siteUrl: string;
readonly title: string;
readonly lang: string;
};
export default ({ siteUrl, page, title, description, lang }: WebsiteSchemaParams): WithContext<WebPage> => ({
"@context": "https://schema.org",
"@type": "WebPage",
"@id": new URL(page, siteUrl).toString(),
"url": new URL(page, siteUrl).toString(),
"name": title,
"description": description,
"inLanguage": lang,
"mainEntity": {
"@type": "WebSite",
"@id": new URL("/", siteUrl).toString(),
},
});

View File

@@ -1,13 +0,0 @@
import type { WithContext, WebSite } from "schema-dts";
export type WebsiteSchemaParams = {
readonly siteUrl: string;
readonly title: string;
};
export default ({ siteUrl, title }: WebsiteSchemaParams): WithContext<WebSite> => ({
"@context": "https://schema.org",
"@type": "WebSite",
"url": new URL("/", siteUrl).toString(),
"name": title,
});