diff --git a/src/components/BackLink.astro b/src/components/BackLink.astro new file mode 100644 index 00000000..5da991ad --- /dev/null +++ b/src/components/BackLink.astro @@ -0,0 +1,34 @@ +--- +interface Props { + href: string; + text: string; +} + +const { href, text } = Astro.props; +--- + +{text} + + diff --git a/src/components/Tag.astro b/src/components/Tag.astro new file mode 100644 index 00000000..4dcb9f40 --- /dev/null +++ b/src/components/Tag.astro @@ -0,0 +1,35 @@ +--- +import { useI18n } from "../i18n/utils"; + +interface Props { + name: string; +} + +const { name } = Astro.props; + +const { translatePath } = useI18n(Astro.url); +--- + + + {name} + + + diff --git a/src/i18n/ch.ts b/src/i18n/ch.ts index 278c674f..663ea59c 100644 --- a/src/i18n/ch.ts +++ b/src/i18n/ch.ts @@ -6,6 +6,10 @@ const ts = { "nav.contact": "Kontakt", "nav.blog": "Blog", "nav.language": "Sprach", + "nav.backToBlog": "Zrügg zum Blog", + "nav.backToServices": "Zrügg zu Services", + "nav.backToJobs": "Zrügg zu Jobs", + "nav.backToCustomers": "Zrügg zu Chunde", // Buttons & CTAs "cta.freeWorkshop": "Gratis Cloud Readiness Workshop", @@ -83,6 +87,7 @@ const ts = { "blog.title": "Blog", "blog.subtitle": "Tipps, Aleitige u Best Practices für modärni Cloud-Büetz", "blog.empty": "No keni Blog-Artikel verfüegbar. Lueg gly mau wieder ine!", + "blog.allPosts": "Alle Artikel", // Customers Page "customers.page.title": "Üsi Chunde", diff --git a/src/i18n/de.ts b/src/i18n/de.ts index 6e4ac148..48365c44 100644 --- a/src/i18n/de.ts +++ b/src/i18n/de.ts @@ -6,6 +6,10 @@ const de = { "nav.contact": "Kontakt", "nav.blog": "Blog", "nav.language": "Sprache", + "nav.backToBlog": "Zurück zum Blog", + "nav.backToServices": "Zurück zu Services", + "nav.backToJobs": "Zurück zu Jobs", + "nav.backToCustomers": "Zurück zu Kunden", // Buttons & CTAs "cta.freeWorkshop": "Kostenloser Cloud Readiness Workshop", @@ -85,6 +89,7 @@ const de = { "blog.subtitle": "Tipps, Anleitungen und Best Practices für moderne Cloud-Entwicklung", "blog.empty": "Noch keine Blog-Artikel verfügbar. Schau bald wieder vorbei!", + "blog.allPosts": "Alle Artikel", // Customers Page "customers.page.title": "Unsere Kunden", diff --git a/src/i18n/en.ts b/src/i18n/en.ts index bb8cd814..99dceb56 100644 --- a/src/i18n/en.ts +++ b/src/i18n/en.ts @@ -6,6 +6,10 @@ const en = { "nav.contact": "Contact", "nav.blog": "Blog", "nav.language": "Language", + "nav.backToBlog": "Back to Blog", + "nav.backToServices": "Back to Services", + "nav.backToJobs": "Back to Jobs", + "nav.backToCustomers": "Back to Customers", // Buttons & CTAs "cta.freeWorkshop": "Free Cloud Readiness Workshop", @@ -83,6 +87,7 @@ const en = { "blog.title": "Blog", "blog.subtitle": "Tips, guides, and best practices to work with cloud tech", "blog.empty": "No blog posts yet. Check back soon!", + "blog.allPosts": "All Posts", // Customers Page "customers.page.title": "Our Customers", diff --git a/src/layouts/CollectionPage.astro b/src/layouts/CollectionPage.astro index 801d3be4..4ea71cc0 100644 --- a/src/layouts/CollectionPage.astro +++ b/src/layouts/CollectionPage.astro @@ -2,6 +2,7 @@ import Layout from "./Layout.astro"; import SEO from "../components/SEO.astro"; import Title from "../components/Title.astro"; +import BackLink from "../components/BackLink.astro"; interface Props { pageTitle: string; @@ -11,10 +12,19 @@ interface Props { lang: string; isEmpty?: boolean; emptyMessage?: string; + backLink?: { href: string; text: string }; } -const { pageTitle, title, subtitle, description, lang, isEmpty, emptyMessage } = - Astro.props; +const { + pageTitle, + title, + subtitle, + description, + lang, + isEmpty, + emptyMessage, + backLink, +} = Astro.props; --- @@ -22,6 +32,13 @@ const { pageTitle, title, subtitle, description, lang, isEmpty, emptyMessage } = <main class="collection-page"> <div class="container"> + { + backLink && ( + <div style="margin-bottom: 1em"> + <BackLink href={backLink.href} text={backLink.text} /> + </div> + ) + } <div class="collection-grid"> <slot /> </div> diff --git a/src/layouts/ContentDetailPage.astro b/src/layouts/ContentDetailPage.astro index 415f2b5a..5a326b09 100644 --- a/src/layouts/ContentDetailPage.astro +++ b/src/layouts/ContentDetailPage.astro @@ -2,6 +2,7 @@ import Layout from "./Layout.astro"; import SEO from "../components/SEO.astro"; import StructuredData from "../components/StructuredData.astro"; +import BackLink from "../components/BackLink.astro"; interface MetaItem { label: string; @@ -44,7 +45,7 @@ const { <main class="content-detail"> <article class="container"> <header class="content-header"> - <a href={backLink.href} class="back-link">{backLink.text}</a> + <BackLink href={backLink.href} text={backLink.text} /> <slot name="header-before" /> <h1>{title}</h1> { @@ -92,18 +93,6 @@ const { margin-bottom: 3rem; } - .back-link { - display: inline-block; - color: var(--color-primary); - text-decoration: none; - margin-bottom: var(--spacing-md); - font-weight: 500; - } - - .back-link:hover { - text-decoration: underline; - } - h1 { font-size: 2.5rem; line-height: 1.2; diff --git a/src/pages/[lang]/about/jobs/[...slug].astro b/src/pages/[lang]/about/jobs/[...slug].astro index 48d0159c..ef497718 100644 --- a/src/pages/[lang]/about/jobs/[...slug].astro +++ b/src/pages/[lang]/about/jobs/[...slug].astro @@ -11,7 +11,7 @@ export async function getStaticPaths() { const { job } = Astro.props; const { Content } = await render(job); -const { lang, translatePath } = useI18n(Astro.url); +const { lang, translatePath, t } = useI18n(Astro.url); const title = `${job.data.title} | Jobs`; const description = `Join bespinian as a ${job.data.title}. ${job.data.location ? "Location: " + job.data.location + "." : ""} ${job.data.employment ? job.data.employment + " position." : ""} We're looking for talented individuals to help companies succeed with cloud-native technologies.`; @@ -29,7 +29,7 @@ if (job.data.employment) metaItems.push({ label: job.data.employment }); {lang} backLink={{ href: translatePath("/about/jobs"), - text: "← Back to Jobs", + text: t("nav.backToJobs"), }} meta={metaItems} structuredData={{ diff --git a/src/pages/[lang]/blog/[...slug].astro b/src/pages/[lang]/blog/[...slug].astro index 86037596..a3533f77 100644 --- a/src/pages/[lang]/blog/[...slug].astro +++ b/src/pages/[lang]/blog/[...slug].astro @@ -5,6 +5,8 @@ import ContentDetailPage from "../../../layouts/ContentDetailPage.astro"; import { getCollectionStaticPaths } from "../../../lib/paths"; import { useI18n } from "../../../i18n/utils"; import { formatDate } from "../../../lib/formatters"; +import Tag from "../../../components/Tag.astro"; +import rss from "../../../assets/rss.svg"; export async function getStaticPaths() { return getCollectionStaticPaths("blog"); @@ -13,7 +15,7 @@ export async function getStaticPaths() { const { post } = Astro.props; const { Content } = await render(post); -const { lang, translatePath } = useI18n(Astro.url); +const { lang, translatePath, t } = useI18n(Astro.url); const formattedDate = formatDate(post.data.pubDate, lang); @@ -28,7 +30,7 @@ const canonicalUrl = new URL(Astro.url.pathname, Astro.site).toString(); title={post.data.title} {description} {lang} - backLink={{ href: translatePath("/blog"), text: "← Back to Blog" }} + backLink={{ href: translatePath("/blog"), text: t("nav.backToBlog") }} meta={[ { label: post.data.author }, { label: formattedDate, datetime: post.data.pubDate.toISOString() }, @@ -61,10 +63,17 @@ const canonicalUrl = new URL(Astro.url.pathname, Astro.site).toString(); } <div slot="header-after" class="tags"> - {post.data.tags.map((tag: string) => <span class="tag">{tag}</span>)} + {post.data.tags.map((tag: string) => <Tag name={tag} />)} </div> <Content /> + <div class="actions"> + <a href={translatePath("/blog")} class="btn">{t("blog.allPosts")}</a> + <a href="/rss.xml" class="btn rss"> + <img width="10" src={rss.src} alt="RSS Feed" /> + RSS + </a> + </div> </ContentDetailPage> <style> @@ -87,12 +96,35 @@ const canonicalUrl = new URL(Astro.url.pathname, Astro.site).toString(); gap: 0.5rem; } - .tag { - background: #f0f0f0; - color: var(--color-gray); - padding: 0.25rem 0.75rem; - border-radius: 12px; + .actions { + margin: 5em 0; + padding-top: 2em; + border-top: 1px solid #cecece; + display: flex; + gap: 1rem; + } + + .btn { + display: inline-flex; + text-decoration: none !important; + color: var(--color-primary); + padding: 6px 16px; + border: 1px solid var(--color-background-primary); + background-color: var(--color-background-secondary); + border-radius: 6px; font-size: 0.875rem; - font-weight: 500; + } + + .btn:hover { + background-color: var(--color-background-primary); + color: var(--color-secondary); + } + + .btn:hover > img { + filter: invert(1); + } + + .btn img { + margin: 0 0.25rem; } </style> diff --git a/src/pages/[lang]/blog/tags/[tag].astro b/src/pages/[lang]/blog/tags/[tag].astro new file mode 100644 index 00000000..773fdbd6 --- /dev/null +++ b/src/pages/[lang]/blog/tags/[tag].astro @@ -0,0 +1,50 @@ +--- +import type { GetStaticPaths } from "astro"; +import { getCollection } from "astro:content"; +import CollectionPage from "../../../../layouts/CollectionPage.astro"; +import BlogCard from "../../../../components/BlogCard.astro"; +import { useI18n } from "../../../../i18n/utils"; +import { languages } from "../../../../i18n/ui"; +import { sortByDateDesc } from "../../../../lib/formatters"; + +export const getStaticPaths = (async () => { + const allPosts = await getCollection("blog"); + + const uniqueTags = [ + ...new Set(allPosts.flatMap((post) => post.data.tags || [])), + ]; + + return uniqueTags.flatMap((tag) => + Object.keys(languages).map((lang) => { + const filteredPosts = allPosts.filter((post) => + post.data.tags?.includes(tag), + ); + const sortedPosts = sortByDateDesc(filteredPosts); + + return { + params: { lang, tag }, + props: { posts: sortedPosts }, + }; + }), + ); +}) satisfies GetStaticPaths; + +const { lang, translatePath, t } = useI18n(Astro.url); +const { tag } = Astro.params; +const { posts } = Astro.props; + +const title = `Blog | ${tag}`; +const description = `Tagged "${tag}"`; +--- + +<CollectionPage + pageTitle={title} + title={t("blog.title")} + subtitle={description} + {description} + {lang} + isEmpty={posts.length === 0} + backLink={{ href: translatePath("/blog"), text: t("nav.backToBlog") }} +> + {posts.map(({ id, data }) => <BlogCard {id} {...data} />)} +</CollectionPage> diff --git a/src/pages/[lang]/blog/tags/index.astro b/src/pages/[lang]/blog/tags/index.astro new file mode 100644 index 00000000..5719a768 --- /dev/null +++ b/src/pages/[lang]/blog/tags/index.astro @@ -0,0 +1,46 @@ +--- +import { getCollection } from "astro:content"; +import { useI18n } from "../../../../i18n/utils"; +import ContentDetailPage from "../../../../layouts/ContentDetailPage.astro"; +import { getLanguagePaths } from "../../../../lib/paths"; +import Tag from "../../../../components/Tag.astro"; + +export function getStaticPaths() { + return getLanguagePaths(); +} + +const { lang, translatePath, t } = useI18n(Astro.url); +const blogPosts = await getCollection("blog"); + +const tagCounts = blogPosts + .flatMap((post: any) => post.data.tags) + .reduce((acc: Record<string, number>, tag: string) => { + acc[tag] = (acc[tag] || 0) + 1; + return acc; + }, {}); + +const tags = Object.entries(tagCounts).sort(([tagA], [tagB]) => + tagA.localeCompare(tagB), +); +--- + +<ContentDetailPage + pageTitle="Tags" + lang={lang} + title="Tags" + description="The blog tags" + backLink={{ href: translatePath("/blog"), text: t("nav.backToBlog") }} +> + <div class="tags"> + {tags.map(([name, _]) => <Tag {name} />)} + </div> +</ContentDetailPage> + +<style> + .tags { + display: flex; + flex-wrap: wrap; + gap: 1rem; + margin-top: 2rem; + } +</style> diff --git a/src/pages/[lang]/customers/[...slug].astro b/src/pages/[lang]/customers/[...slug].astro index d2ddb11f..92e2e9fa 100644 --- a/src/pages/[lang]/customers/[...slug].astro +++ b/src/pages/[lang]/customers/[...slug].astro @@ -29,7 +29,7 @@ const canonicalUrl = new URL(Astro.url.pathname, Astro.site).toString(); {lang} backLink={{ href: translatePath("/customers"), - text: "← Back to Customers", + text: t("nav.backToCustomers"), }} meta={[ { label: customer.data.company }, diff --git a/src/pages/[lang]/services/[...slug].astro b/src/pages/[lang]/services/[...slug].astro index 8928f2cc..6708fc2c 100644 --- a/src/pages/[lang]/services/[...slug].astro +++ b/src/pages/[lang]/services/[...slug].astro @@ -8,6 +8,7 @@ import CTA from "../../../components/CTA.astro"; import { languages } from "../../../i18n/ui"; import { useI18n } from "../../../i18n/utils"; import spaceship from "../../../assets/spaceship1.svg"; +import BackLink from "../../../components/BackLink.astro"; export async function getStaticPaths() { const allServices = await getCollection("services"); @@ -54,9 +55,10 @@ const canonicalUrl = new URL(Astro.url.pathname, Astro.site).toString(); <article> <div class="container"> <header class="service-header"> - <a href={translatePath("/#services")} class="back-link"> - ← Back to Services - </a> + <BackLink + href={translatePath("/#services")} + text={t("nav.backToServices")} + /> </header> </div> @@ -146,18 +148,6 @@ const canonicalUrl = new URL(Astro.url.pathname, Astro.site).toString(); margin-bottom: 3rem; } - .back-link { - display: inline-block; - color: var(--color-primary); - text-decoration: none; - margin-bottom: var(--spacing-md); - font-weight: 500; - } - - .back-link:hover { - text-decoration: underline; - } - h1 { font-size: 3.5rem; line-height: 1.2; diff --git a/src/styles/global.css b/src/styles/global.css index d49e75cb..81f4342b 100644 --- a/src/styles/global.css +++ b/src/styles/global.css @@ -12,6 +12,8 @@ --color-background-gray: #efefef; --color-blockquote-bg: #f9f9f9; + --color-gray-light: #f0f0f0; + /* Spacing */ --spacing-xs: 0.5rem; --spacing-sm: 1rem;