mirror of
https://github.com/morten-olsen/morten-olsen.github.io.git
synced 2026-02-08 01:46:28 +01:00
rewrite
This commit is contained in:
226
src/pages/about.astro
Normal file
226
src/pages/about.astro
Normal file
@@ -0,0 +1,226 @@
|
||||
---
|
||||
import { Picture } from "astro:assets";
|
||||
import Html from "~/components/page/Html.astro";
|
||||
import { data } from "~/data/data"
|
||||
import { formatMonthYear, positionWithTeam } from "~/utils/utils.format";
|
||||
|
||||
const { Content, ...profile } = data.profile;
|
||||
const experiences = await data.experiences.getAll();
|
||||
const skills = await data.skills.getAll();
|
||||
const jsonLd = await profile.getJsonLd();
|
||||
---
|
||||
|
||||
<Html
|
||||
title={profile.name}
|
||||
description={profile.name}
|
||||
jsonLd={jsonLd}
|
||||
>
|
||||
<main class="main">
|
||||
<div class="content">
|
||||
<Content />
|
||||
</div>
|
||||
|
||||
<div class="profiles">
|
||||
<h2>Profiles</h2>
|
||||
<ul>
|
||||
{Object.entries(profile.profiles).map(([id, profile]) => (
|
||||
<li class="profile">
|
||||
<a href={profile.url} target="_blank">{profile.network?.name || id}</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="experiences">
|
||||
<h2>Experiences</h2>
|
||||
<div class="content">
|
||||
{experiences.map((experience) => (
|
||||
<div data-fadein class="experience">
|
||||
{experience.data.logo && (
|
||||
<div class="logo">
|
||||
<Picture
|
||||
alt='thumbnail image'
|
||||
fetchpriority="auto"
|
||||
src={experience.data.logo}
|
||||
formats={['avif', 'webp', 'jpeg']}
|
||||
width={50}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div class="header">
|
||||
<a class="companyName" href={`/experiences/${experience.id}`}><h3>{experience.data.company.name}</h3></a>
|
||||
<a class="position" href={`/experiences/${experience.id}`}><h3>{positionWithTeam(experience.data.position.name, experience.data.position.team)}</h3></a>
|
||||
</div>
|
||||
<div class="time">
|
||||
<div class="from">
|
||||
{formatMonthYear(experience.data.startDate)}
|
||||
</div>
|
||||
<div class="to">
|
||||
{formatMonthYear(experience.data.endDate)}
|
||||
</div>
|
||||
</div>
|
||||
{experience.data.stack && (
|
||||
<div class="stack">
|
||||
<ul>
|
||||
{experience.data.stack.map((item) => (
|
||||
<li>{item}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="skills">
|
||||
<h2>Technologies</h2>
|
||||
{skills.map((skill) => (
|
||||
<div class="skill" data-fadein>
|
||||
<a class="name" href={`/skills/${skill.id}`}><h3>{skill.data.name}</h3></a>
|
||||
<div class="technologies">
|
||||
<ul>
|
||||
{skill.data.technologies.map((technology) => (
|
||||
<li>{technology}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</main>
|
||||
</Html>
|
||||
|
||||
<script src="../scripts/fadein.ts"></script>
|
||||
|
||||
<style>
|
||||
.main {
|
||||
max-width: var(--content-width);
|
||||
margin: auto;
|
||||
display: grid;
|
||||
gap: calc(var(--gap) * 3);
|
||||
grid-template-columns: 200px 1fr;
|
||||
grid-template-rows: auto;
|
||||
grid-template-areas:
|
||||
"content content"
|
||||
"profiles profiles"
|
||||
"skills experiences";
|
||||
|
||||
h2 {
|
||||
font-weight: var(--fw-md);
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
grid-area: content;
|
||||
}
|
||||
|
||||
.profiles {
|
||||
grid-area: profiles;
|
||||
display: flex;
|
||||
gap: var(--gap);
|
||||
|
||||
ul {
|
||||
display: contents;
|
||||
}
|
||||
}
|
||||
|
||||
.skills {
|
||||
grid-area: skills;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--gap);
|
||||
|
||||
.skill {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--gap);
|
||||
|
||||
.technologies ul {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: calc(var(--gap) / 2);
|
||||
|
||||
li {
|
||||
border: solid var(--c-line) 1px;
|
||||
padding: 3px 8px;
|
||||
border-radius: var(--radius);
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.experiences {
|
||||
grid-area: experiences;
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: calc(var(--gap) * 2);
|
||||
}
|
||||
|
||||
.experience {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr auto;
|
||||
grid-template-rows: auto;
|
||||
column-gap: var(--gap);
|
||||
row-gap: calc(var(--gap) / 2);
|
||||
align-items: center;
|
||||
grid-template-areas:
|
||||
"logo header time"
|
||||
"logo stack time";
|
||||
|
||||
.logo {
|
||||
grid-area: logo;
|
||||
|
||||
img {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
border-radius: 50%;
|
||||
object-fit: cover;
|
||||
object-position: center center;
|
||||
}
|
||||
}
|
||||
|
||||
.time {
|
||||
grid-area: time;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.companyName {
|
||||
grid-area: companyName;
|
||||
font-weight: var(--fw-md);
|
||||
}
|
||||
|
||||
.position {
|
||||
grid-area: position;
|
||||
font-size: var(--fs-sm);
|
||||
}
|
||||
|
||||
.summary{
|
||||
grid-area: summary;
|
||||
}
|
||||
|
||||
.stack {
|
||||
grid-area: stack;
|
||||
ul {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: calc(var(--gap) / 4);
|
||||
|
||||
li {
|
||||
border: solid var(--c-line) 1px;
|
||||
padding: 3px 8px;
|
||||
border-radius: var(--radius);
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,20 +0,0 @@
|
||||
---
|
||||
import { type Article, data } from '@/data/data.js'
|
||||
import ArticleView from '@/layouts/article/article.astro'
|
||||
|
||||
type Props = {
|
||||
article: Article
|
||||
}
|
||||
|
||||
export const getStaticPaths = async () => {
|
||||
const articles = await data.articles.find()
|
||||
return articles.map((article) => ({
|
||||
params: { slug: article.data.slug },
|
||||
props: { article }
|
||||
}))
|
||||
}
|
||||
const { props } = Astro
|
||||
const { article } = props
|
||||
---
|
||||
|
||||
<ArticleView article={article} />
|
||||
@@ -1,36 +0,0 @@
|
||||
---
|
||||
import { type Article, data } from '@/data/data.js'
|
||||
import Articles from '@/layouts/articles/articles.astro'
|
||||
import { range } from '@/utils/data.js'
|
||||
|
||||
type Props = {
|
||||
articles: Article[]
|
||||
pageNumber: number
|
||||
pageCount: number
|
||||
pageSize: number
|
||||
}
|
||||
|
||||
export async function getStaticPaths() {
|
||||
const pageSize = 2
|
||||
const allArticles = await data.articles.find()
|
||||
const pageCount = Math.ceil(allArticles.length / pageSize)
|
||||
const pages = range(0, pageCount).map((index) => {
|
||||
const start = index * pageSize
|
||||
const end = start + pageSize
|
||||
return {
|
||||
pageNumber: index + 1,
|
||||
pageCount,
|
||||
pageSize,
|
||||
articles: allArticles.slice(start, end)
|
||||
}
|
||||
})
|
||||
return pages.map((page) => ({
|
||||
params: { page: String(page.pageNumber) },
|
||||
props: page
|
||||
}))
|
||||
}
|
||||
|
||||
const { props } = Astro
|
||||
---
|
||||
|
||||
<Articles {...props} />
|
||||
@@ -1,18 +0,0 @@
|
||||
import rss from '@astrojs/rss';
|
||||
import type { APIContext } from 'astro';
|
||||
|
||||
import { data } from '@/data/data.ts';
|
||||
|
||||
export async function GET(context: APIContext) {
|
||||
const articles = await data.articles.find();
|
||||
const profile = data.profile;
|
||||
return rss({
|
||||
title: profile.basics.name,
|
||||
description: profile.basics.tagline,
|
||||
site: context.site || 'http://localhost:3000',
|
||||
items: articles.map((article) => ({
|
||||
...article.data,
|
||||
link: `/articles/${article.data.slug}/`,
|
||||
})),
|
||||
});
|
||||
}
|
||||
47
src/pages/experiences/[id].astro
Normal file
47
src/pages/experiences/[id].astro
Normal file
@@ -0,0 +1,47 @@
|
||||
---
|
||||
import type { GetStaticPaths } from "astro";
|
||||
import { Picture } from "astro:assets";
|
||||
import { render } from "astro:content";
|
||||
import Html from "~/components/page/Html.astro";
|
||||
import { data } from "~/data/data";
|
||||
import { formatMonthYear, positionWithTeam } from "~/utils/utils.format";
|
||||
|
||||
export const getStaticPaths = (async () => {
|
||||
const experiences = await data.experiences.getAll();
|
||||
return experiences.map((experience) => ({
|
||||
params: { id: experience.id }
|
||||
}));
|
||||
}) satisfies GetStaticPaths;
|
||||
|
||||
const experience = await data.experiences.get(Astro.params.id)
|
||||
const { Content } = await render(experience);
|
||||
---
|
||||
|
||||
<Html
|
||||
title={experience.data.slug}
|
||||
description={experience.data.slug}
|
||||
>
|
||||
<main>
|
||||
<h1>{experience.data.company.name}</h1>
|
||||
<h2>{positionWithTeam(experience.data.position.name, experience.data.position.team)}</h2>
|
||||
{experience.data.logo && (
|
||||
<Picture
|
||||
alt='thumbnail image'
|
||||
fetchpriority="auto"
|
||||
src={experience.data.logo}
|
||||
formats={['avif', 'webp', 'jpeg']}
|
||||
width={50}
|
||||
/>
|
||||
)}
|
||||
{formatMonthYear(experience.data.startDate)}
|
||||
{formatMonthYear(experience.data.endDate)}
|
||||
<Content />
|
||||
</main>
|
||||
</Html>
|
||||
|
||||
<style>
|
||||
main {
|
||||
max-width: var(--content-width);
|
||||
margin: auto;
|
||||
}
|
||||
</style>
|
||||
@@ -1,6 +1,89 @@
|
||||
---
|
||||
import {} from '@/data/data.js'
|
||||
import Frontpage from '@/layouts/frontpage/frontpage.astro'
|
||||
import { Picture } from "astro:assets";
|
||||
import AbsoluteTime from "~/components/base/time/AbsoluteTime.astro";
|
||||
import Html from "~/components/page/Html.astro";
|
||||
import { data } from "~/data/data"
|
||||
|
||||
const { Content, ...profile } = data.profile;
|
||||
const posts = await data.posts.getPublished();
|
||||
const jsonLd = await profile.getJsonLd();
|
||||
---
|
||||
|
||||
<Frontpage />
|
||||
<Html
|
||||
title={profile.name}
|
||||
description={profile.name}
|
||||
jsonLd={jsonLd}
|
||||
>
|
||||
<main class="main">
|
||||
<div class="content">
|
||||
<Content />
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<h2>Posts</h2>
|
||||
<div class="posts">
|
||||
{posts.map((post) => (
|
||||
<div class="post" data-fadein>
|
||||
<a href={`/posts/${post.id}`}>
|
||||
<Picture
|
||||
alt='thumbnail image'
|
||||
src={post.data.heroImage}
|
||||
formats={['avif', 'webp', 'jpeg']}
|
||||
fetchpriority="auto"
|
||||
widths={[100, 200, 300]}
|
||||
/>
|
||||
</a>
|
||||
<a class="title" href={`/posts/${post.id}`}><h3>{post.data.title}</h3></a>
|
||||
<div class="subtitle"><AbsoluteTime datetime={post.data.pubDate} /></div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</main>
|
||||
</Html>
|
||||
|
||||
<script src="../scripts/fadein.ts"></script>
|
||||
|
||||
<style>
|
||||
.main {
|
||||
max-width: var(--content-width);
|
||||
margin: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: calc(var(--gap) * 3);
|
||||
padding-bottom: 50px;
|
||||
|
||||
h2 {
|
||||
font-weight: var(--fw-md);
|
||||
}
|
||||
}
|
||||
|
||||
.posts {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: calc(var(--gap) * 2);
|
||||
|
||||
.post {
|
||||
img {
|
||||
width: 100%;
|
||||
max-height: 250px;
|
||||
object-fit: cover;
|
||||
border-radius: var(--radius);
|
||||
box-shadow: 0 0 40px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.title {
|
||||
font-weight: var(--fw-md);
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: var(--fs-sm);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
import type { ManifestOptions } from 'vite-plugin-pwa'
|
||||
|
||||
import { icons } from '@/assets/images/icons.js'
|
||||
import { data } from '@/data/data.js'
|
||||
import { icons } from '~/assets/images/images.icons';
|
||||
import { data } from '~/data/data';
|
||||
|
||||
export async function GET() {
|
||||
const [maskableIcon] = icons.pngs.filter(
|
||||
(icon) => icon.size === '512x512' && icon.src.includes('png')
|
||||
)
|
||||
const nonMaskableIcons = icons.pngs.filter((icon) => icon !== maskableIcon)
|
||||
const basics = data.profile.basics
|
||||
const { profile } = data;
|
||||
|
||||
const manifest: Partial<ManifestOptions> = {
|
||||
name: basics.name,
|
||||
short_name: basics.name,
|
||||
description: basics.tagline,
|
||||
name: profile.name,
|
||||
short_name: profile.name,
|
||||
// description: basics.tagline,
|
||||
theme_color: '#30E130',
|
||||
background_color: data.site.theme,
|
||||
// background_color: data.site.theme,
|
||||
start_url: '/',
|
||||
display: 'minimal-ui',
|
||||
icons: [
|
||||
@@ -26,13 +26,13 @@ export async function GET() {
|
||||
})),
|
||||
...(maskableIcon
|
||||
? [
|
||||
{
|
||||
src: maskableIcon.src,
|
||||
sizes: maskableIcon.size,
|
||||
type: 'image/png',
|
||||
purpose: 'maskable'
|
||||
}
|
||||
]
|
||||
{
|
||||
src: maskableIcon.src,
|
||||
sizes: maskableIcon.size,
|
||||
type: 'image/png',
|
||||
purpose: 'maskable'
|
||||
}
|
||||
]
|
||||
: [])
|
||||
]
|
||||
}
|
||||
|
||||
255
src/pages/posts/[id]/index.astro
Normal file
255
src/pages/posts/[id]/index.astro
Normal file
@@ -0,0 +1,255 @@
|
||||
---
|
||||
import Html from '~/components/page/Html.astro';
|
||||
import type { GetStaticPaths } from "astro";
|
||||
import { render } from "astro:content";
|
||||
import { data } from '~/data/data'
|
||||
import { Picture } from 'astro:assets';
|
||||
|
||||
export const getStaticPaths = (async () => {
|
||||
const posts = await data.posts.getPublished();
|
||||
return posts.map((post) => ({
|
||||
params: { id: post.id }
|
||||
}));
|
||||
}) satisfies GetStaticPaths;
|
||||
|
||||
const post = await data.posts.get(Astro.params.id);
|
||||
const { Content } = await render(post);
|
||||
---
|
||||
|
||||
<Html
|
||||
title={post.data.title}
|
||||
description={post.data.description}
|
||||
jsonLd={post.jsonLd}
|
||||
image={`/posts/${post.id}/share.png`}
|
||||
>
|
||||
<article>
|
||||
<header>
|
||||
<h1>
|
||||
{post.data.title.split(' ').map((word) => <span>{word}</span>)}
|
||||
</h1>
|
||||
<a href='/'><h2>By {data.profile.name}</h2></a>
|
||||
</header>
|
||||
<Picture
|
||||
loading='eager'
|
||||
class='img'
|
||||
src={post.data.heroImage}
|
||||
widths={[320, 640, 1024, 1400]}
|
||||
formats={['avif', 'webp', 'png']}
|
||||
alt='Cover image'
|
||||
/>
|
||||
<div class='content'>
|
||||
<Content />
|
||||
</div>
|
||||
</article>
|
||||
</Html>
|
||||
|
||||
<style>
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
font-family: var(--font-family-heading);
|
||||
font-weight: 700;
|
||||
line-height: 1.2;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
|
||||
article {
|
||||
--left-padding: 100px;
|
||||
font-family: var(--font-family);
|
||||
font-size: var(--font-size);
|
||||
letter-spacing: var(--letter-spacing);
|
||||
color: var(--color);
|
||||
background-color: var(--background-color);
|
||||
display: grid;
|
||||
letter-spacing: 0.05rem;
|
||||
font-size: 1rem;
|
||||
line-height: 1.3rem;
|
||||
grid-template-columns: 1fr calc(60ch + var(--left-padding)) 2fr;
|
||||
grid-template-rows: auto;
|
||||
grid-template-areas:
|
||||
'. title cover'
|
||||
'. content cover';
|
||||
}
|
||||
|
||||
article :global(picture) {
|
||||
grid-area: cover;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.img {
|
||||
max-width: 100%;
|
||||
height: 100vh;
|
||||
top: 0;
|
||||
position: sticky;
|
||||
object-fit: cover;
|
||||
object-position: center;
|
||||
right: 0;
|
||||
clip-path: polygon(40% 0%, 100% 0%, 100% 100%, 0% 100%, 0% 50%);
|
||||
}
|
||||
|
||||
header {
|
||||
grid-area: title;
|
||||
min-height: 80vh;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
|
||||
.subtitle {
|
||||
font-size: 1rem;
|
||||
line-height: 2rem;
|
||||
padding-top: 1rem;
|
||||
padding-bottom: 1rem;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 300;
|
||||
color: #fff;
|
||||
text-transform: uppercase;
|
||||
margin-top: var(--space-md);
|
||||
color: #000;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
h1 {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
font-size: 4rem;
|
||||
line-height: 1;
|
||||
color: #fff;
|
||||
text-transform: uppercase;
|
||||
font-weight: 400;
|
||||
gap: 1rem;
|
||||
|
||||
span {
|
||||
display: inline-block;
|
||||
background: red;
|
||||
padding: 0.5rem 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
grid-area: content;
|
||||
padding: var(--space-xl);
|
||||
padding-left: var(--left-padding);
|
||||
|
||||
img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
margin-bottom: var(--space-lg);
|
||||
}
|
||||
|
||||
p {
|
||||
text-align: justify;
|
||||
margin-bottom: var(--space-md);
|
||||
line-height: 1.5rem;
|
||||
}
|
||||
|
||||
p:first-of-type {
|
||||
&:first-letter {
|
||||
font-size: 5rem;
|
||||
border: 5px solid #000;
|
||||
float: left;
|
||||
padding: 0 var(--space-md);
|
||||
margin-right: 1rem;
|
||||
line-height: 1;
|
||||
}
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 1.2rem;
|
||||
font-weight: 600;
|
||||
margin-bottom: var(--space-lg);
|
||||
padding-top: var(--space-lg);
|
||||
text-transform: uppercase;
|
||||
|
||||
&:first-letter {
|
||||
background: #000;
|
||||
color: #fff;
|
||||
padding: 0 5px;
|
||||
letter-spacing: -0.2rem;
|
||||
}
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 1rem;
|
||||
text-transform: uppercase;
|
||||
font-weight: 900;
|
||||
margin-bottom: 1rem;
|
||||
|
||||
&:first-letter {
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
}
|
||||
|
||||
strong {
|
||||
background: #000;
|
||||
padding: 0 5px;
|
||||
color: #fff;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
em {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
li {
|
||||
list-style-type: circle;
|
||||
margin-left: 2rem;
|
||||
padding-bottom: var(--space-md);
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: monospace;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
article {
|
||||
--left-padding: 0;
|
||||
grid-template-columns: 1fr;
|
||||
grid-template-areas:
|
||||
'title'
|
||||
'cover'
|
||||
'content';
|
||||
}
|
||||
|
||||
article picture {
|
||||
position: absolute;
|
||||
z-index: -1;
|
||||
height: 80vh;
|
||||
}
|
||||
|
||||
.img {
|
||||
clip-path: none;
|
||||
height: 80vh;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
header {
|
||||
padding: var(--space-xl);
|
||||
height: 80vh;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 2.5rem;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: var(--space-lg);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
35
src/pages/posts/[id]/share.png.ts
Normal file
35
src/pages/posts/[id]/share.png.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import type { APIContext, GetStaticPaths } from 'astro';
|
||||
import { data } from '~/data/data';
|
||||
import { createCanvas } from 'canvas';
|
||||
|
||||
const getStaticPaths = (async () => {
|
||||
const posts = await data.posts.getPublished();
|
||||
return posts.map((post) => ({
|
||||
params: { id: post.id }
|
||||
}));
|
||||
}) satisfies GetStaticPaths;
|
||||
|
||||
const GET = async (context: APIContext<Record<string, string>, { id: string }>) => {
|
||||
const { id } = context.params;
|
||||
const post = await data.posts.get(id);
|
||||
const canvas = createCanvas(200, 200)
|
||||
const ctx = canvas.getContext('2d')
|
||||
|
||||
ctx.fillText(post.data.title, 10, 10)
|
||||
const buffer = await new Promise<Buffer>((resolve, reject) => {
|
||||
canvas.toBuffer((err, result) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
resolve(result);
|
||||
}
|
||||
})
|
||||
})
|
||||
return new Response(buffer, {
|
||||
headers: {
|
||||
'Content-Type': 'image/png',
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export { GET, getStaticPaths }
|
||||
17
src/pages/posts/index.astro
Normal file
17
src/pages/posts/index.astro
Normal file
@@ -0,0 +1,17 @@
|
||||
---
|
||||
import { getCollection } from 'astro:content';
|
||||
import Html from '~/components/page/Html.astro';
|
||||
|
||||
const posts = await getCollection('posts');
|
||||
---
|
||||
|
||||
<Html
|
||||
title="Morten Olsen's Posts"
|
||||
description='Foo'
|
||||
>
|
||||
<h1>Posts</h1>
|
||||
|
||||
{posts.map((post) => (
|
||||
<a href={`/posts/${post.id}`}>{post.data.title}</a>
|
||||
))}
|
||||
</Html>
|
||||
19
src/pages/posts/rss.xml.ts
Normal file
19
src/pages/posts/rss.xml.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import rss from '@astrojs/rss';
|
||||
import type { APIContext } from 'astro';
|
||||
import { data } from '~/data/data';
|
||||
|
||||
|
||||
const GET = async (context: APIContext) => {
|
||||
const posts = await data.posts.getPublished();
|
||||
return rss({
|
||||
title: data.profile.name,
|
||||
description: 'Bar',
|
||||
site: context.site || 'http://localhost:3000',
|
||||
items: posts.map((post) => ({
|
||||
...post.data,
|
||||
link: `/posts/${post.id}`,
|
||||
})),
|
||||
});
|
||||
}
|
||||
|
||||
export { GET }
|
||||
@@ -1,6 +1,6 @@
|
||||
import { data } from '@/data/data.ts'
|
||||
import { data } from "~/data/data"
|
||||
|
||||
export async function GET() {
|
||||
const resume = await data.getJsonResume()
|
||||
const resume = await data.profile.getResumeJson();
|
||||
return new Response(JSON.stringify(resume, null, 2))
|
||||
}
|
||||
|
||||
18
src/pages/skills/[id].astro
Normal file
18
src/pages/skills/[id].astro
Normal file
@@ -0,0 +1,18 @@
|
||||
---
|
||||
import type { GetStaticPaths } from "astro";
|
||||
import { render } from "astro:content";
|
||||
import { data } from "~/data/data";
|
||||
|
||||
export const getStaticPaths = (async () => {
|
||||
const skills = await data.skills.getAll();
|
||||
return skills.map((skill) => ({
|
||||
params: { id: skill.id }
|
||||
}));
|
||||
}) satisfies GetStaticPaths;
|
||||
|
||||
const skill= await data.skills.get(Astro.params.id)
|
||||
const { Content } = await render(skill);
|
||||
---
|
||||
|
||||
<h1>Hello {skill.data.name}</h1>
|
||||
<Content />
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
import Work from '@/layouts/work-history/work-history.astro'
|
||||
---
|
||||
|
||||
<Work />
|
||||
Reference in New Issue
Block a user