Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
118112f0b7
|
|||
|
47a0acab13
|
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"image": "mcr.microsoft.com/devcontainers/javascript-node:24",
|
||||
"image": "mcr.microsoft.com/devcontainers/javascript-node:22",
|
||||
"forwardPorts": [4321],
|
||||
"portsAttributes": {
|
||||
"4321": {
|
||||
|
||||
2
.env
Normal file
2
.env
Normal file
@@ -0,0 +1,2 @@
|
||||
DEFAULT_TITLE=Valentin Popov’s Blog
|
||||
DEFAULT_DESCRIPTION=Tech insights and coding best practices from an OpenSource enthusiast and ethical hacker.
|
||||
@@ -1,28 +0,0 @@
|
||||
name: RenovateBot
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: "@daily"
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
|
||||
jobs:
|
||||
renovate:
|
||||
container: ghcr.io/renovatebot/renovate:43
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Run renovate
|
||||
run: |
|
||||
renovate
|
||||
env:
|
||||
GITHUB_COM_TOKEN: ${{ secrets.RENOVATE_GITHUB_TOKEN }}
|
||||
LOG_LEVEL: ${{ vars.RENOVATE_LOG_LEVEL }}
|
||||
RENOVATE_CONFIG_FILE: renovate.config.cjs
|
||||
RENOVATE_LOG_LEVEL: ${{ vars.RENOVATE_LOG_LEVEL }}
|
||||
RENOVATE_REPOSITORIES: ${{ gitea.repository }}
|
||||
RENOVATE_TOKEN: ${{ secrets.RENOVATE_TOKEN }}
|
||||
@@ -1,16 +0,0 @@
|
||||
name: Test
|
||||
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: npm test
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
- run: npm ci
|
||||
- run: npm run check
|
||||
- run: npm run typecheck
|
||||
16
.github/renovate.json
vendored
Normal file
16
.github/renovate.json
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||
"extends": ["config:recommended", ":disableDependencyDashboard"],
|
||||
"assignees": ["valentineus"],
|
||||
"labels": ["dependencies", "automated"],
|
||||
"packageRules": [
|
||||
{
|
||||
"description": "Group patch & minor updates together",
|
||||
"groupName": "all digest updates",
|
||||
"groupSlug": "all-digest",
|
||||
"matchUpdateTypes": ["minor", "patch", "pin", "digest"],
|
||||
"matchPackageNames": ["*"],
|
||||
"automerge": true
|
||||
}
|
||||
]
|
||||
}
|
||||
26
.github/workflows/ci.yml
vendored
Normal file
26
.github/workflows/ci.yml
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
pull_request:
|
||||
branches: [master]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: Test
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
- name: Run checks
|
||||
run: npm run check
|
||||
@@ -30,7 +30,7 @@ export default {
|
||||
},
|
||||
],
|
||||
plugins: ["prettier-plugin-astro"],
|
||||
printWidth: 256,
|
||||
printWidth: 120,
|
||||
proseWrap: "never",
|
||||
quoteProps: "consistent",
|
||||
requirePragma: false,
|
||||
|
||||
30
.renovaterc
30
.renovaterc
@@ -1,30 +0,0 @@
|
||||
{
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||
"extends": [
|
||||
"config:recommended",
|
||||
":disableDependencyDashboard"
|
||||
],
|
||||
"assignees": [
|
||||
"valentineus"
|
||||
],
|
||||
"labels": [
|
||||
"dependencies",
|
||||
"automated"
|
||||
],
|
||||
"packageRules": [
|
||||
{
|
||||
"groupName": "all digest updates",
|
||||
"groupSlug": "all-digest",
|
||||
"matchUpdateTypes": [
|
||||
"minor",
|
||||
"patch",
|
||||
"pin",
|
||||
"digest"
|
||||
],
|
||||
"matchPackageNames": [
|
||||
"*"
|
||||
],
|
||||
"automerge": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
# popov.link
|
||||
|
||||
[](https://512kb.club)
|
||||
[](https://github.com/valentineus/popov.link/actions/workflows/ci.yml)
|
||||
|
||||
Personal website source code built with [Astro](https://astro.build/).
|
||||
|
||||
@@ -41,9 +41,9 @@ npm run preview
|
||||
|
||||
## Project Info
|
||||
|
||||
- Issues: [GitHub](https://github.com/valentineus/popov.link/issues)
|
||||
- Read-only mirror: [git.popov.link](https://git.popov.link/popov.link/)
|
||||
- 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)
|
||||
|
||||
## Comments
|
||||
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
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(), ogImages()],
|
||||
integrations: [sitemap()],
|
||||
build: {
|
||||
inlineStylesheets: "always",
|
||||
},
|
||||
|
||||
2715
package-lock.json
generated
2715
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
11
package.json
11
package.json
@@ -3,7 +3,7 @@
|
||||
"type": "module",
|
||||
"version": "2025.01.24",
|
||||
"private": true,
|
||||
"packageManager": "npm@11.9.0",
|
||||
"packageManager": "npm@11.4.1",
|
||||
"browserslist": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
@@ -11,7 +11,6 @@
|
||||
],
|
||||
"scripts": {
|
||||
"format": "prettier --write .",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"dev": "astro dev",
|
||||
"start": "astro dev",
|
||||
"check": "astro check",
|
||||
@@ -23,22 +22,14 @@
|
||||
"@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": "^15.0.0",
|
||||
"gray-matter": "^4.0.3",
|
||||
"mdast-util-to-string": "^4.0.0",
|
||||
"reading-time": "^1.5.0",
|
||||
"sass": "^1.89.1",
|
||||
"satori": "^0.19.0",
|
||||
"satori-html": "^0.3.2",
|
||||
"schema-dts": "^1.1.5",
|
||||
"sharp": "^0.34.2",
|
||||
"typescript": "^5"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 389 KiB |
2
public/images/preview/.gitignore
vendored
2
public/images/preview/.gitignore
vendored
@@ -1,2 +0,0 @@
|
||||
*
|
||||
!.gitignore
|
||||
@@ -1,6 +0,0 @@
|
||||
module.exports = {
|
||||
endpoint: "https://code.popov.link",
|
||||
gitAuthor: "renovate[bot] <renovatebot@noreply.localhost>",
|
||||
optimizeForDisabled: true,
|
||||
platform: "gitea",
|
||||
};
|
||||
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.
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.
@@ -1 +1,2 @@
|
||||
<script is:inline defer src="https://appmetrix.com/pixel/T5X0z12SoASBV8Dv"></script>
|
||||
<!-- AppMetrix -->
|
||||
<script is:inline src="https://appmetrix.com/pixel/T5X0z12SoASBV8Dv"></script>
|
||||
|
||||
@@ -15,7 +15,6 @@ const theme = "transparent_dark";
|
||||
|
||||
<script
|
||||
is:inline
|
||||
defer
|
||||
src="https://giscus.app/client.js"
|
||||
data-category-id={categoryId}
|
||||
data-category={category}
|
||||
|
||||
@@ -1,22 +1,14 @@
|
||||
---
|
||||
import type { WithContext, Thing } from "schema-dts";
|
||||
import JsonLd from "./JsonLd.astro";
|
||||
|
||||
type Props = {
|
||||
readonly description: string;
|
||||
readonly preview: string;
|
||||
readonly schema: WithContext<Thing>;
|
||||
readonly title: string;
|
||||
};
|
||||
|
||||
const { description, preview, schema, title } = Astro.props;
|
||||
|
||||
const canonicalUrl = new URL(Astro.url.pathname, Astro.site);
|
||||
const previewUrl = new URL(preview, Astro.site);
|
||||
const canonicalURL = new URL(Astro.url.pathname, Astro.site);
|
||||
const { description, title } = Astro.props;
|
||||
---
|
||||
|
||||
<head>
|
||||
<!-- Meta Tags -->
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
<meta http-equiv="content-type" content="text/html; charset=utf-8" />
|
||||
|
||||
@@ -26,29 +18,13 @@ const previewUrl = new URL(preview, Astro.site);
|
||||
|
||||
<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>
|
||||
|
||||
@@ -1,6 +1,27 @@
|
||||
<style lang="scss">
|
||||
@use "../scss/variables" as *;
|
||||
|
||||
header {
|
||||
padding-bottom: 1rem;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.site-title {
|
||||
color: $colorText;
|
||||
font-weight: bold;
|
||||
left: 0;
|
||||
position: absolute;
|
||||
text-decoration: none;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.nav-links {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
a {
|
||||
margin-right: 1.5rem;
|
||||
text-decoration: none;
|
||||
|
||||
&:last-child {
|
||||
margin-right: 0;
|
||||
@@ -9,8 +30,9 @@
|
||||
</style>
|
||||
|
||||
<header>
|
||||
<nav aria-label="Navigation">
|
||||
<a href="/" lang="en" aria-label="Home">Home</a>
|
||||
<a href="/blog/" lang="en" aria-label="Blog">Blog</a>
|
||||
</nav>
|
||||
<a class="site-title" href="/">{import.meta.env.DEFAULT_TITLE}</a>
|
||||
<div class="nav-links">
|
||||
<a href="/">Home</a>
|
||||
<a href="/blog/">Blog</a>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
<style lang="scss">
|
||||
@use "../../scss/variables" as *;
|
||||
|
||||
a {
|
||||
color: $colorText;
|
||||
display: inline-block;
|
||||
margin: 0 0.5rem;
|
||||
}
|
||||
|
||||
svg {
|
||||
vertical-align: middle;
|
||||
}
|
||||
</style>
|
||||
|
||||
<a href="mailto:valentin@popov.link" title="E-Mail" rel="noopener" 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" 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>
|
||||
</a>
|
||||
@@ -1,22 +0,0 @@
|
||||
<style lang="scss">
|
||||
@use "../../scss/variables" as *;
|
||||
|
||||
a {
|
||||
color: $colorText;
|
||||
display: inline-block;
|
||||
margin: 0 0.5rem;
|
||||
}
|
||||
|
||||
svg {
|
||||
vertical-align: middle;
|
||||
}
|
||||
</style>
|
||||
|
||||
<a href="https://github.com/valentineus" title="GitHub" rel="noopener" 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" 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"
|
||||
>
|
||||
</path>
|
||||
</svg>
|
||||
</a>
|
||||
@@ -1,21 +0,0 @@
|
||||
<style lang="scss">
|
||||
@use "../../scss/variables" as *;
|
||||
|
||||
a {
|
||||
color: $colorText;
|
||||
display: inline-block;
|
||||
margin: 0 0.5rem;
|
||||
}
|
||||
|
||||
svg {
|
||||
vertical-align: middle;
|
||||
}
|
||||
</style>
|
||||
|
||||
<a href="https://www.linkedin.com/in/valentineus/" title="LinkedIn" rel="noopener" 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" 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>
|
||||
</svg>
|
||||
</a>
|
||||
@@ -1,17 +0,0 @@
|
||||
<style lang="scss">
|
||||
a {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
svg {
|
||||
vertical-align: middle;
|
||||
}
|
||||
</style>
|
||||
|
||||
<a href="/feed.xml" title="RSS Feed" rel="noopener" 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" 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>
|
||||
</svg>
|
||||
</a>
|
||||
@@ -1,13 +0,0 @@
|
||||
---
|
||||
import type { WithContext, Thing } from "schema-dts";
|
||||
|
||||
type Props = {
|
||||
readonly schema: WithContext<Thing>;
|
||||
};
|
||||
|
||||
const { schema } = Astro.props;
|
||||
const json = JSON.stringify(schema);
|
||||
---
|
||||
|
||||
<!-- JSON-LD -->
|
||||
<script is:inline type="application/ld+json" set:html={json} />
|
||||
@@ -8,18 +8,12 @@ type Props = {
|
||||
|
||||
const { post } = Astro.props;
|
||||
const { remarkPluginFrontmatter } = await post.render();
|
||||
|
||||
const formattedDate = dayjs(post.data.datePublished.toString()).format("MMMM DD, YYYY");
|
||||
const datePublished = post.data.datePublished.toISOString();
|
||||
const formattedDate = dayjs(post.data.pubDate.toString()).format("MMMM DD, YYYY");
|
||||
---
|
||||
|
||||
<style lang="scss">
|
||||
@use "../scss/variables" as *;
|
||||
|
||||
a {
|
||||
color: $colorText;
|
||||
}
|
||||
|
||||
small {
|
||||
font-size: $fontSizeBase * 0.75;
|
||||
opacity: 0.5;
|
||||
@@ -27,14 +21,12 @@ const datePublished = post.data.datePublished.toISOString();
|
||||
</style>
|
||||
|
||||
<li>
|
||||
<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>
|
||||
<a href={`/blog/${post.slug}`}>{post.data.title}</a>
|
||||
<div>
|
||||
<small>
|
||||
<time datetime={post.data.pubDate.toISOString()}>{formattedDate}</time>
|
||||
<span>•</span>
|
||||
<span>{remarkPluginFrontmatter.minutesRead}</span>
|
||||
</small>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
49
src/components/PostSummary.astro
Normal file
49
src/components/PostSummary.astro
Normal file
@@ -0,0 +1,49 @@
|
||||
---
|
||||
import { type CollectionEntry } from "astro:content";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
type Props = {
|
||||
readonly post: CollectionEntry<"blog">;
|
||||
};
|
||||
|
||||
const { post } = Astro.props;
|
||||
const { remarkPluginFrontmatter } = await post.render();
|
||||
const formattedDate = dayjs(post.data.pubDate.toString()).format("MMMM DD, YYYY");
|
||||
---
|
||||
|
||||
<style lang="scss">
|
||||
@use "../scss/variables" as *;
|
||||
|
||||
a {
|
||||
color: $colorText;
|
||||
display: block;
|
||||
padding-bottom: 3rem;
|
||||
|
||||
&:visited {
|
||||
color: $colorText;
|
||||
}
|
||||
}
|
||||
|
||||
h2 {
|
||||
color: $colorBlossom;
|
||||
font-size: 1.25em;
|
||||
margin: 0.5em 0;
|
||||
}
|
||||
|
||||
div {
|
||||
font-size: $fontSizeBase * 0.75;
|
||||
opacity: 0.5;
|
||||
}
|
||||
</style>
|
||||
|
||||
<a href={`/blog/${post.slug}`}>
|
||||
<article>
|
||||
<div>
|
||||
<time datetime={post.data.pubDate.toISOString()}>{formattedDate}</time>
|
||||
<span>•</span>
|
||||
<span>{remarkPluginFrontmatter.minutesRead}</span>
|
||||
</div>
|
||||
<h2>{post.data.title}</h2>
|
||||
<p>{post.data.description}</p>
|
||||
</article>
|
||||
</a>
|
||||
@@ -1,43 +0,0 @@
|
||||
---
|
||||
import { getCollection } from "astro:content";
|
||||
import dayjs from "dayjs";
|
||||
import RSSIcon from "../Icons/RSS.astro";
|
||||
|
||||
const posts = await getCollection("blog", ({ data }) => {
|
||||
return data.draft !== true;
|
||||
});
|
||||
|
||||
posts.sort((a, b) => b.data.datePublished.getTime() - a.data.datePublished.getTime());
|
||||
|
||||
const latestPosts = posts.slice(0, 5);
|
||||
---
|
||||
|
||||
<style lang="scss">
|
||||
@use "../../scss/variables" as *;
|
||||
|
||||
small {
|
||||
font-size: $fontSizeBase * 0.75;
|
||||
opacity: 0.5;
|
||||
}
|
||||
</style>
|
||||
|
||||
<section>
|
||||
<h2>Latest posts <RSSIcon /></h2>
|
||||
<ul>
|
||||
{
|
||||
latestPosts.map((post) => (
|
||||
<li>
|
||||
<a href={`/blog/${post.slug}`} lang={post.data.lang}>
|
||||
{post.data.title}
|
||||
</a>
|
||||
|
||||
<small>
|
||||
<time datetime={post.data.datePublished.toISOString()} lang="en">
|
||||
{dayjs(post.data.datePublished.toString()).format("MMMM DD, YYYY")}
|
||||
</time>
|
||||
</small>
|
||||
</li>
|
||||
))
|
||||
}
|
||||
</ul>
|
||||
</section>
|
||||
@@ -1,19 +0,0 @@
|
||||
---
|
||||
import GitHubIcon from "../Icons/GitHub.astro";
|
||||
import LinkedInIcon from "../Icons/LinkedIn.astro";
|
||||
import EmailIcon from "../Icons/Email.astro";
|
||||
---
|
||||
|
||||
<style lang="scss">
|
||||
div {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
<section>
|
||||
<div>
|
||||
<GitHubIcon />
|
||||
<LinkedInIcon />
|
||||
<EmailIcon />
|
||||
</div>
|
||||
</section>
|
||||
@@ -1,7 +0,0 @@
|
||||
<section>
|
||||
<div>
|
||||
<h1>Hi, I'm Valentin 👋</h1>
|
||||
<p>I'm a professional software developer currently working as a project manager and team lead. On my personal website, I share thoughts on tech, leadership, and digital life.</p>
|
||||
<p>Welcome, and feel free to explore!</p>
|
||||
</div>
|
||||
</section>
|
||||
@@ -1,29 +0,0 @@
|
||||
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",
|
||||
},
|
||||
};
|
||||
@@ -1,10 +1,8 @@
|
||||
---
|
||||
basedOn: "https://adrianhenke.wordpress.com/2008/12/05/create-lib-file-from-dll/"
|
||||
title: "Create .lib file from .dll (archive)"
|
||||
description: "Quick guide to create a .lib from a .dll on Windows: list exports with dumpbin, make a .def file, then generate the import library with lib."
|
||||
datePublished: "2023-05-04"
|
||||
dateModified: "2023-05-04"
|
||||
lang: "en"
|
||||
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."
|
||||
---
|
||||
|
||||
> This's a copy of a non-my post. The original article [is here](https://adrianhenke.wordpress.com/2008/12/05/create-lib-file-from-dll/) ([archive](https://web.archive.org/web/20161118122539/https://adrianhenke.wordpress.com/2008/12/05/create-lib-file-from-dll/)).
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
---
|
||||
title: "Горячая перезагрузка ElectronJS приложения"
|
||||
description: "Горячая перезагрузка ElectronJS: перезапуск main через nodemon и автообновление renderer с HMR/chokidar. Пошагово, без electron-reload и с Webpack."
|
||||
datePublished: "2019-08-15"
|
||||
dateModified: "2019-08-15"
|
||||
lang: "ru"
|
||||
author: "Valentin Popov"
|
||||
pubDate: "2019-08-15"
|
||||
description: "Руководство по автоматической перезагрузке приложений на Electron с помощью пакетов electron-reload и electron-webpack. Обход проблем с совместимостью и использование HMR для renderer процесса."
|
||||
---
|
||||
|
||||
## Main процесс
|
||||
|
||||
@@ -1,9 +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
|
||||
---
|
||||
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
---
|
||||
title: 'Получение исходного кода "Chromium Projects"'
|
||||
description: "Как получить и подготовить исходники Chromium на Windows: Visual Studio, Cygwin, depot_tools, команды gclient. Краткая пошаговая инструкция."
|
||||
datePublished: "2012-01-30"
|
||||
dateModified: "2012-01-30"
|
||||
lang: "ru"
|
||||
author: "Valentin Popov"
|
||||
pubDate: "2012-01-30"
|
||||
description: "Изучение исходных кодов Chromium: подготовка системы и установка необходимых программных компонентов. Руководство для начинающих разработчиков. Получите инструкции по установке Microsoft Visual Studio, Cygwin, Python и других инструментов. Действительно на январь-февраль 2012 года."
|
||||
---
|
||||
|
||||
> Перенос [оригинальной статьи](https://adeptus-mechanicus.blogspot.com/2012/01/chromium-projects.html) 2012 года из моего [старого блога](https://adeptus-mechanicus.blogspot.com/) ([зеркало](https://web.archive.org/web/20160217052148/http://adeptus-mechanicus.blogspot.com/)).
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
---
|
||||
title: "Установка Moodle в Fedora"
|
||||
description: "Установка Moodle в Fedora: как исправить зависание инсталлятора и cURL error из-за SELinux. Правильные setsebool и chcon для доступа к сети и каталогам."
|
||||
datePublished: "2018-07-23"
|
||||
dateModified: "2018-07-23"
|
||||
lang: "ru"
|
||||
author: "Valentin Popov"
|
||||
pubDate: "2018-07-23"
|
||||
description: "Решение проблем установки Moodle из-за SELinux: как настроить правила доступа для устранения ошибок в веб-интерфейсе и при работе с cURL. Практические советы и команды."
|
||||
---
|
||||
|
||||
Во время установки Moodle, сталкиваешься со следующими проблемами:
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
---
|
||||
title: "Компиляция Rust на TL-MR3020"
|
||||
description: "Кросс-компиляция Rust для OpenWrt на TL-MR3020 (MIPS): rustup, cross-rs, Docker/Podman, UPX, пример TCP-сервера и сжатие бинарника."
|
||||
datePublished: "2023-05-01"
|
||||
dateModified: "2023-05-01"
|
||||
lang: "ru"
|
||||
author: "Valentin Popov"
|
||||
pubDate: "2023-05-01"
|
||||
description: 'Как настроить и оптимизировать проект Rust для кросс-компиляции на TP-Link TL-MR3020 с использованием Fedora Linux 38 и OpenWrt 22.03.4. Шаг за шагом от базового "Hello, World!" до асинхронного TCP сервера.'
|
||||
---
|
||||
|
||||
Информация в статье актуальна для дистрибутива [Fedora Linux 38](https://docs.fedoraproject.org/en-US/releases/f38/), прошивки [OpenWrt 22.03.4](https://openwrt.org/releases/22.03/notes-22.03.4) и устройства [TP-Link TL-MR3020](https://www.tp-link.com/en/home-networking/3g-4g-router/tl-mr3020/) ревизии v3.20.
|
||||
|
||||
@@ -3,12 +3,10 @@ import { defineCollection, z } from "astro:content";
|
||||
const blog = defineCollection({
|
||||
type: "content",
|
||||
schema: z.object({
|
||||
basedOn: z.optional(z.string()),
|
||||
dateModified: z.coerce.date(),
|
||||
datePublished: z.coerce.date(),
|
||||
author: z.string(),
|
||||
description: z.string(),
|
||||
draft: z.optional(z.boolean()),
|
||||
lang: z.string(),
|
||||
pubDate: z.coerce.date(),
|
||||
title: z.string(),
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
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`);
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -1,23 +1,22 @@
|
||||
---
|
||||
import type { WithContext, Thing } from "schema-dts";
|
||||
import Analytics from "../components/Analytics.astro";
|
||||
import Head from "../components/Head.astro";
|
||||
import Header from "../components/Header.astro";
|
||||
import "../scss/global.scss";
|
||||
|
||||
type Props = {
|
||||
readonly description: string;
|
||||
readonly lang: string;
|
||||
readonly preview: string;
|
||||
readonly schema: WithContext<Thing>;
|
||||
readonly title: string;
|
||||
readonly description?: string;
|
||||
readonly title?: string;
|
||||
};
|
||||
|
||||
const { description, lang, preview, schema, title } = Astro.props;
|
||||
const { description, title } = Astro.props;
|
||||
---
|
||||
|
||||
<html lang={lang}>
|
||||
<Head title={title} description={description} preview={preview} schema={schema} />
|
||||
<html lang="ru">
|
||||
<Head
|
||||
title={title ?? import.meta.env.DEFAULT_TITLE}
|
||||
description={description ?? import.meta.env.DEFAULT_DESCRIPTION}
|
||||
/>
|
||||
|
||||
<body>
|
||||
<main>
|
||||
|
||||
@@ -1,30 +1,17 @@
|
||||
---
|
||||
import { config } from "../config";
|
||||
import Layout from "../layouts/BaseLayout.astro";
|
||||
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 = pageSchema({
|
||||
siteUrl: new URL("/", Astro.site).toString(),
|
||||
page: "/404",
|
||||
title,
|
||||
description,
|
||||
lang,
|
||||
});
|
||||
---
|
||||
|
||||
<Layout title={title} description={description} preview={preview} lang={lang} schema={schema}>
|
||||
<div style={{ "text-align": "center" }}>
|
||||
<Layout>
|
||||
<div style="text-align:center;">
|
||||
<h1>404</h1>
|
||||
<p><strong>Page not found</strong></p>
|
||||
<p>
|
||||
<small>
|
||||
If you see this message, please
|
||||
<a href=`mailto:valentin@popov.link?subject=${encodeURIComponent('I found a broken page')}`>let me know</a>
|
||||
<a href=`mailto:valentin@popov.link?subject=${encodeURIComponent('I found a broken page')}`>
|
||||
let me know
|
||||
</a>
|
||||
</small>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
import { type CollectionEntry, getCollection } from "astro:content";
|
||||
import Comments from "../../components/Comments.astro";
|
||||
import Layout from "../../layouts/BaseLayout.astro";
|
||||
import blogPostSchema from "../../utils/schemas/blogPostSchema";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
type Props = CollectionEntry<"blog">;
|
||||
@@ -19,31 +18,8 @@ export async function getStaticPaths() {
|
||||
}
|
||||
|
||||
const post = Astro.props;
|
||||
|
||||
const { Content, remarkPluginFrontmatter } = await post.render();
|
||||
|
||||
const description = post.data.description;
|
||||
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 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,
|
||||
});
|
||||
const formattedDate = dayjs(post.data.pubDate.toString()).format("MMMM DD, YYYY");
|
||||
---
|
||||
|
||||
<style lang="scss">
|
||||
@@ -54,20 +30,23 @@ const schema = blogPostSchema({
|
||||
}
|
||||
</style>
|
||||
|
||||
<Layout title={title} description={description} preview={preview} lang={lang} schema={schema}>
|
||||
<Layout description={post.data.description} title={post.data.title}>
|
||||
<article>
|
||||
<header>
|
||||
<h1>{title}</h1>
|
||||
<section>
|
||||
<h1>{post.data.title}</h1>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<p>
|
||||
<small>
|
||||
Posted
|
||||
<time datetime={datePublished} lang="en">{formattedDate}</time>
|
||||
<time datetime={post.data.pubDate.toISOString()}>{formattedDate}</time>
|
||||
by {post.data.author}
|
||||
<span> • </span>
|
||||
<span>{remarkPluginFrontmatter.minutesRead}</span>
|
||||
</small>
|
||||
</p>
|
||||
</header>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<Content />
|
||||
|
||||
@@ -1,61 +1,19 @@
|
||||
---
|
||||
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 RSSIcon from "../../components/Icons/RSS.astro";
|
||||
|
||||
const posts = await getCollection("blog", ({ data }) => {
|
||||
return data.draft !== true;
|
||||
});
|
||||
|
||||
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.datePublished.getFullYear().toString();
|
||||
if (!acc[year]) {
|
||||
acc[year] = [];
|
||||
}
|
||||
acc[year].push(post);
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
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,
|
||||
});
|
||||
posts.sort((a, b) => b.data.pubDate.getTime() - a.data.pubDate.getTime());
|
||||
---
|
||||
|
||||
<Layout title={title} description={description} preview={preview} lang={lang} schema={schema}>
|
||||
<section>
|
||||
<h1>
|
||||
Blog posts
|
||||
<RSSIcon />
|
||||
</h1>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
{
|
||||
years.map((year) => (
|
||||
<div>
|
||||
<h2>{year}</h2>
|
||||
<ul>
|
||||
{postsByYear[year].map((post) => (
|
||||
<PostElement post={post} />
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
))
|
||||
}
|
||||
<Layout>
|
||||
<section style={{ "margin-top": "3rem" }}>
|
||||
<ul>
|
||||
{posts.map((post) => <PostElement post={post} />)}
|
||||
</ul>
|
||||
</section>
|
||||
</Layout>
|
||||
|
||||
@@ -2,16 +2,13 @@ import { getCollection } from "astro:content";
|
||||
import rss from "@astrojs/rss";
|
||||
|
||||
export async function GET(context) {
|
||||
const title = "RSS Feed | Valentin Popov Blog";
|
||||
const description = "Follow the latest posts from Valentin Popov via RSS.";
|
||||
|
||||
const posts = await getCollection("blog", ({ data }) => {
|
||||
return data.draft !== true;
|
||||
});
|
||||
|
||||
return rss({
|
||||
customData: `<language>en</language>`,
|
||||
description: description,
|
||||
customData: `<language>ru-ru</language>`,
|
||||
description: import.meta.env.DEFAULT_DESCRIPTION,
|
||||
items: posts.map((post) => ({
|
||||
customData: post.data.customData,
|
||||
description: post.data.description,
|
||||
@@ -20,6 +17,6 @@ export async function GET(context) {
|
||||
title: post.data.title,
|
||||
})),
|
||||
site: context.site,
|
||||
title: title,
|
||||
title: import.meta.env.DEFAULT_TITLE,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,27 +1,17 @@
|
||||
---
|
||||
import { config } from "../config";
|
||||
import LatestPostsSection from "../components/Sections/LatestPosts.astro";
|
||||
import { getCollection } from "astro:content";
|
||||
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 PostSummary from "../components/PostSummary.astro";
|
||||
|
||||
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 = pageSchema({
|
||||
siteUrl: new URL("/", Astro.site).toString(),
|
||||
page: "/",
|
||||
title,
|
||||
description,
|
||||
lang,
|
||||
const posts = await getCollection("blog", ({ data }) => {
|
||||
return data.draft !== true;
|
||||
});
|
||||
|
||||
posts.sort((a, b) => b.data.pubDate.getTime() - a.data.pubDate.getTime());
|
||||
---
|
||||
|
||||
<Layout title={title} description={description} preview={preview} lang={lang} schema={schema}>
|
||||
<WelcomeSection />
|
||||
<SocialLinksSection />
|
||||
<LatestPostsSection />
|
||||
<Layout>
|
||||
<section style={{ "margin-top": "3rem" }}>
|
||||
{posts.map((post) => <PostSummary post={post} />)}
|
||||
</section>
|
||||
</Layout>
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
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();
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
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");
|
||||
})(),
|
||||
};
|
||||
@@ -1,37 +0,0 @@
|
||||
import type { WithContext, BlogPosting } from "schema-dts";
|
||||
import { config } from "../../config";
|
||||
|
||||
export type BlogPostSchemaParams = {
|
||||
readonly dateModified: string;
|
||||
readonly datePublished: 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, 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": config.author.name,
|
||||
"url": config.author.url,
|
||||
"sameAs": config.author.sameAs,
|
||||
},
|
||||
"mainEntityOfPage": {
|
||||
"@type": "WebPage",
|
||||
"@id": new URL(`/blog/${slug}`, siteUrl).toString(),
|
||||
},
|
||||
...(isBasedOn && { isBasedOn: isBasedOn }),
|
||||
});
|
||||
@@ -1,26 +0,0 @@
|
||||
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, posts }: BlogSchemaParams): WithContext<CollectionPage> => ({
|
||||
"@context": "https://schema.org",
|
||||
"@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,
|
||||
})),
|
||||
},
|
||||
});
|
||||
@@ -1,23 +0,0 @@
|
||||
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(),
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user