feat: implement pagination for posts list and add category sidebar
All checks were successful
Deploy / deploy (push) Successful in 46s
All checks were successful
Deploy / deploy (push) Successful in 46s
This commit is contained in:
parent
6c09629c45
commit
7c45526444
9 changed files with 275 additions and 56 deletions
|
|
@ -29,7 +29,7 @@ Astro 6 static site, based on the `blog` starter template. No tests, no linter c
|
||||||
- Because posts sit two levels deep, hero images and component imports inside MD/MDX use `../../../assets/…` / `../../../components/…` relative paths.
|
- Because posts sit two levels deep, hero images and component imports inside MD/MDX use `../../../assets/…` / `../../../components/…` relative paths.
|
||||||
- UI strings and path helpers live in [src/i18n/ui.ts](src/i18n/ui.ts). `t(locale, key)` for translations; `localizePath(path, locale)` prefixes `/en` when needed; `switchLocalePath(pathname, target)` rewrites the current URL to the other locale (used by the header language switcher and hreflang alternates in [BaseHead.astro](src/components/BaseHead.astro)).
|
- UI strings and path helpers live in [src/i18n/ui.ts](src/i18n/ui.ts). `t(locale, key)` for translations; `localizePath(path, locale)` prefixes `/en` when needed; `switchLocalePath(pathname, target)` rewrites the current URL to the other locale (used by the header language switcher and hreflang alternates in [BaseHead.astro](src/components/BaseHead.astro)).
|
||||||
- Site titles/descriptions per locale live in [src/consts.ts](src/consts.ts) (`SITE.de`, `SITE.en`). The `SITE[locale]` map is the single source of truth — update when rebranding.
|
- Site titles/descriptions per locale live in [src/consts.ts](src/consts.ts) (`SITE.de`, `SITE.en`). The `SITE[locale]` map is the single source of truth — update when rebranding.
|
||||||
- Pages: German under `src/pages/` (`index.astro`, `about.astro`, `[...slug].astro`, `rss.xml.js`), English mirrors under `src/pages/en/`. The shared home UI lives in [src/components/HomePage.astro](src/components/HomePage.astro); both `index.astro` files are thin wrappers that pass `locale="de"` / `locale="en"`.
|
- Pages: German under `src/pages/` (`index.astro`, `about.astro`, `[...slug].astro`, `rss.xml.js`), English mirrors under `src/pages/en/`. The shared home UI lives in [src/components/PostsList.astro](src/components/PostsList.astro); both `index.astro` files are thin wrappers that pass `locale="de"` / `locale="en"`.
|
||||||
- Layouts: [BaseLayout.astro](src/layouts/BaseLayout.astro) is the common skeleton (`<html>` / `<head>` with `BaseHead` / `<body>` with `Header` + `main` + `Footer`). Accepts `title`, `description`, `locale`, optional `image`, optional `entry` (for the language-switch translation lookup), optional `bodyClass`, and a `head` named slot for per-page `<link>`/`<meta>` extras. All page templates compose via this layout — don't re-assemble head/header/footer by hand. [Post.astro](src/layouts/Post.astro) wraps `BaseLayout` to add hero image + title block + category line for single posts. `Header`, `BaseHead`, and `FormattedDate` also accept `locale` directly and fall back to `getLocaleFromUrl(Astro.url)`.
|
- Layouts: [BaseLayout.astro](src/layouts/BaseLayout.astro) is the common skeleton (`<html>` / `<head>` with `BaseHead` / `<body>` with `Header` + `main` + `Footer`). Accepts `title`, `description`, `locale`, optional `image`, optional `entry` (for the language-switch translation lookup), optional `bodyClass`, and a `head` named slot for per-page `<link>`/`<meta>` extras. All page templates compose via this layout — don't re-assemble head/header/footer by hand. [Post.astro](src/layouts/Post.astro) wraps `BaseLayout` to add hero image + title block + category line for single posts. `Header`, `BaseHead`, and `FormattedDate` also accept `locale` directly and fall back to `getLocaleFromUrl(Astro.url)`.
|
||||||
- Separate RSS feeds per locale: `/rss.xml` (de) and `/en/rss.xml`. The sitemap integration is configured with `i18n: { defaultLocale: 'de', locales: { de: 'de-DE', en: 'en-US' } }`.
|
- Separate RSS feeds per locale: `/rss.xml` (de) and `/en/rss.xml`. The sitemap integration is configured with `i18n: { defaultLocale: 'de', locales: { de: 'de-DE', en: 'en-US' } }`.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,24 +3,64 @@ import { Image } from 'astro:assets';
|
||||||
import FormattedDate from '~/components/FormattedDate.astro';
|
import FormattedDate from '~/components/FormattedDate.astro';
|
||||||
import BaseLayout from '~/layouts/BaseLayout.astro';
|
import BaseLayout from '~/layouts/BaseLayout.astro';
|
||||||
import { type Locale, SITE } from '~/consts';
|
import { type Locale, SITE } from '~/consts';
|
||||||
import { getPostsByLocale, postSlug } from '~/i18n/posts';
|
import {
|
||||||
import { localizePath } from '~/i18n/ui';
|
categoryHref,
|
||||||
|
getCategoriesByLocale,
|
||||||
|
getPostsByCategory,
|
||||||
|
getPostsByLocale,
|
||||||
|
paginationSegment,
|
||||||
|
POSTS_PER_PAGE,
|
||||||
|
postSlug,
|
||||||
|
} from '~/i18n/posts';
|
||||||
|
import { localizePath, t } from '~/i18n/ui';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
locale: Locale;
|
locale: Locale;
|
||||||
|
page?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { locale } = Astro.props;
|
const { locale, page = 1 } = Astro.props;
|
||||||
const posts = await getPostsByLocale(locale);
|
const all = await getPostsByLocale(locale);
|
||||||
|
const categories = await getCategoriesByLocale(locale);
|
||||||
|
const categoriesWithCounts = (
|
||||||
|
await Promise.all(
|
||||||
|
categories.map(async (c) => ({
|
||||||
|
category: c,
|
||||||
|
count: (await getPostsByCategory(c)).length,
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
).filter((c) => c.count > 0);
|
||||||
|
const totalPages = Math.max(1, Math.ceil(all.length / POSTS_PER_PAGE));
|
||||||
|
const currentPage = Math.min(Math.max(1, page), totalPages);
|
||||||
|
const posts = all.slice((currentPage - 1) * POSTS_PER_PAGE, currentPage * POSTS_PER_PAGE);
|
||||||
|
|
||||||
|
const segment = paginationSegment(locale);
|
||||||
|
const homeHref = localizePath('/', locale);
|
||||||
|
const newerHref =
|
||||||
|
currentPage === 2
|
||||||
|
? homeHref
|
||||||
|
: currentPage > 2
|
||||||
|
? localizePath(`/${segment}/${currentPage - 1}/`, locale)
|
||||||
|
: undefined;
|
||||||
|
const olderHref =
|
||||||
|
currentPage < totalPages
|
||||||
|
? localizePath(`/${segment}/${currentPage + 1}/`, locale)
|
||||||
|
: undefined;
|
||||||
|
const pageLabel = t(locale, 'pagination.page')
|
||||||
|
.replace('{current}', String(currentPage))
|
||||||
|
.replace('{total}', String(totalPages));
|
||||||
|
const pageTitle =
|
||||||
|
currentPage === 1 ? SITE[locale].title : `${SITE[locale].title} – ${pageLabel}`;
|
||||||
---
|
---
|
||||||
|
|
||||||
<BaseLayout
|
<BaseLayout
|
||||||
title={SITE[locale].title}
|
title={pageTitle}
|
||||||
description={SITE[locale].description}
|
description={SITE[locale].description}
|
||||||
locale={locale}
|
locale={locale}
|
||||||
bodyClass="home"
|
bodyClass="home"
|
||||||
>
|
>
|
||||||
<section>
|
<div class="home-grid">
|
||||||
|
<section class="posts">
|
||||||
<ul>
|
<ul>
|
||||||
{
|
{
|
||||||
posts.map((post) => (
|
posts.map((post) => (
|
||||||
|
|
@ -28,91 +68,217 @@ const posts = await getPostsByLocale(locale);
|
||||||
<a href={localizePath(`/${postSlug(post)}/`, locale)}>
|
<a href={localizePath(`/${postSlug(post)}/`, locale)}>
|
||||||
{post.data.heroImage && (
|
{post.data.heroImage && (
|
||||||
<Image
|
<Image
|
||||||
width={720}
|
width={240}
|
||||||
height={360}
|
height={120}
|
||||||
src={post.data.heroImage}
|
src={post.data.heroImage}
|
||||||
alt=""
|
alt=""
|
||||||
transition:name={`hero-${post.id}`}
|
transition:name={`hero-${post.id}`}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<h4 class="title">{post.data.title}</h4>
|
<div class="meta">
|
||||||
<p class="date">
|
<h4 class="title">{post.data.title}</h4>
|
||||||
<FormattedDate date={post.data.pubDate} locale={locale} />
|
<p class="date">
|
||||||
</p>
|
<FormattedDate date={post.data.pubDate} locale={locale} />
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
</ul>
|
</ul>
|
||||||
|
{
|
||||||
|
totalPages > 1 && (
|
||||||
|
<nav class="pagination" aria-label="pagination">
|
||||||
|
<div class="pagination-nav">
|
||||||
|
{newerHref ? (
|
||||||
|
<a class="pagination-link prev" href={newerHref} rel="prev">
|
||||||
|
← {t(locale, 'pagination.newer')}
|
||||||
|
</a>
|
||||||
|
) : (
|
||||||
|
<span />
|
||||||
|
)}
|
||||||
|
<span class="pagination-info">{pageLabel}</span>
|
||||||
|
{olderHref ? (
|
||||||
|
<a class="pagination-link next" href={olderHref} rel="next">
|
||||||
|
{t(locale, 'pagination.older')} →
|
||||||
|
</a>
|
||||||
|
) : (
|
||||||
|
<span />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
)
|
||||||
|
}
|
||||||
</section>
|
</section>
|
||||||
|
{
|
||||||
|
categoriesWithCounts.length > 0 && (
|
||||||
|
<aside class="sidebar" aria-label={t(locale, 'nav.categories')}>
|
||||||
|
<h2 class="sidebar-title">{t(locale, 'nav.categories')}</h2>
|
||||||
|
<ul class="sidebar-list">
|
||||||
|
{categoriesWithCounts.map(({ category, count }) => (
|
||||||
|
<li>
|
||||||
|
<a href={categoryHref(category)}>
|
||||||
|
<span class="name">{category.data.name}</span>
|
||||||
|
<span class="count">{count}</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</aside>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</div>
|
||||||
</BaseLayout>
|
</BaseLayout>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
body.home main {
|
.home-grid {
|
||||||
width: 960px;
|
display: grid;
|
||||||
|
grid-template-columns: minmax(0, 1fr) 240px;
|
||||||
|
gap: 3rem;
|
||||||
|
align-items: start;
|
||||||
}
|
}
|
||||||
ul {
|
.posts ul {
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 2rem;
|
|
||||||
list-style-type: none;
|
list-style-type: none;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
ul li {
|
.posts ul li {
|
||||||
width: calc(50% - 1rem);
|
margin-bottom: 1.5rem;
|
||||||
|
padding-bottom: 1.5rem;
|
||||||
|
border-bottom: 1px solid rgb(var(--gray-light));
|
||||||
}
|
}
|
||||||
ul li * {
|
.posts ul li:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
.posts ul li a {
|
||||||
|
display: flex;
|
||||||
|
gap: 1.25rem;
|
||||||
|
align-items: center;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
transition: 0.2s ease;
|
transition: 0.2s ease;
|
||||||
}
|
}
|
||||||
ul li:first-child {
|
.posts ul li img {
|
||||||
width: 100%;
|
width: 160px;
|
||||||
margin-bottom: 1rem;
|
height: 80px;
|
||||||
text-align: center;
|
object-fit: cover;
|
||||||
|
border-radius: 8px;
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
ul li:first-child img {
|
.meta {
|
||||||
width: 100%;
|
flex: 1;
|
||||||
}
|
min-width: 0;
|
||||||
ul li:first-child .title {
|
|
||||||
font-size: 2.369rem;
|
|
||||||
}
|
|
||||||
ul li img {
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
border-radius: 12px;
|
|
||||||
}
|
|
||||||
ul li a {
|
|
||||||
display: block;
|
|
||||||
}
|
}
|
||||||
.title {
|
.title {
|
||||||
margin: 0;
|
margin: 0 0 0.25rem;
|
||||||
color: rgb(var(--black));
|
color: rgb(var(--black));
|
||||||
line-height: 1;
|
line-height: 1.2;
|
||||||
|
font-size: 1.25rem;
|
||||||
}
|
}
|
||||||
.date {
|
.date {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
color: rgb(var(--gray));
|
color: rgb(var(--gray));
|
||||||
|
font-size: 0.9rem;
|
||||||
}
|
}
|
||||||
ul li a:hover h4,
|
.posts ul li a:hover .title,
|
||||||
ul li a:hover .date {
|
.posts ul li a:hover .date {
|
||||||
color: rgb(var(--accent));
|
color: rgb(var(--accent));
|
||||||
}
|
}
|
||||||
ul a:hover img {
|
.posts ul li a:hover img {
|
||||||
box-shadow: var(--box-shadow);
|
box-shadow: var(--box-shadow);
|
||||||
}
|
}
|
||||||
@media (max-width: 720px) {
|
.sidebar {
|
||||||
ul {
|
position: sticky;
|
||||||
gap: 0.5em;
|
top: 2rem;
|
||||||
|
}
|
||||||
|
.sidebar-title {
|
||||||
|
margin: 0 0 1rem;
|
||||||
|
font-size: 1rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
color: rgb(var(--gray));
|
||||||
|
}
|
||||||
|
.sidebar-list {
|
||||||
|
list-style: none;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
border-top: 1px solid rgb(var(--gray-light));
|
||||||
|
}
|
||||||
|
.sidebar-list li {
|
||||||
|
border-bottom: 1px solid rgb(var(--gray-light));
|
||||||
|
}
|
||||||
|
.sidebar-list a {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: baseline;
|
||||||
|
gap: 0.5rem;
|
||||||
|
padding: 0.6rem 0;
|
||||||
|
text-decoration: none;
|
||||||
|
color: rgb(var(--black));
|
||||||
|
transition: 0.2s ease;
|
||||||
|
}
|
||||||
|
.sidebar-list a:hover {
|
||||||
|
color: rgb(var(--accent));
|
||||||
|
}
|
||||||
|
.sidebar-list .count {
|
||||||
|
color: rgb(var(--gray));
|
||||||
|
font-size: 0.85rem;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
}
|
||||||
|
.pagination {
|
||||||
|
margin-top: 2rem;
|
||||||
|
}
|
||||||
|
.pagination-nav {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr auto 1fr;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
.pagination-link {
|
||||||
|
color: rgb(var(--black));
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: 600;
|
||||||
|
transition: 0.2s ease;
|
||||||
|
}
|
||||||
|
.pagination-link:hover {
|
||||||
|
color: rgb(var(--accent));
|
||||||
|
}
|
||||||
|
.pagination-link.prev {
|
||||||
|
justify-self: start;
|
||||||
|
}
|
||||||
|
.pagination-link.next {
|
||||||
|
justify-self: end;
|
||||||
|
}
|
||||||
|
.pagination-info {
|
||||||
|
color: rgb(var(--gray));
|
||||||
|
font-size: 0.9rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
@media (max-width: 900px) {
|
||||||
|
.home-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: 2.5rem;
|
||||||
}
|
}
|
||||||
ul li {
|
.sidebar {
|
||||||
|
position: static;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
.posts ul li a {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
.posts ul li img {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
text-align: center;
|
height: auto;
|
||||||
|
aspect-ratio: 2 / 1;
|
||||||
}
|
}
|
||||||
ul li:first-child {
|
.pagination-nav {
|
||||||
margin-bottom: 0;
|
grid-template-columns: 1fr 1fr;
|
||||||
}
|
}
|
||||||
ul li:first-child .title {
|
.pagination-info {
|
||||||
font-size: 1.563em;
|
grid-column: 1 / -1;
|
||||||
|
order: -1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -148,6 +148,14 @@ export function tagIndexSegment(locale: Locale): string {
|
||||||
return locale === 'de' ? 'schlagwoerter' : 'tags';
|
return locale === 'de' ? 'schlagwoerter' : 'tags';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** URL segment used for paginated home routes per locale. */
|
||||||
|
export function paginationSegment(locale: Locale): string {
|
||||||
|
return locale === 'de' ? 'seite' : 'page';
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Number of posts shown per page on the home index. */
|
||||||
|
export const POSTS_PER_PAGE = 10;
|
||||||
|
|
||||||
export function categoryHref(category: CollectionEntry<'categories'>): string {
|
export function categoryHref(category: CollectionEntry<'categories'>): string {
|
||||||
const locale = entryLocale(category);
|
const locale = entryLocale(category);
|
||||||
return localizePath(`/${categorySegment(locale)}/${entrySlug(category)}/`, locale);
|
return localizePath(`/${categorySegment(locale)}/${entrySlug(category)}/`, locale);
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,9 @@ export const ui = {
|
||||||
'webmentions.reposts': 'Reposts',
|
'webmentions.reposts': 'Reposts',
|
||||||
'webmentions.replies': 'Antworten',
|
'webmentions.replies': 'Antworten',
|
||||||
'webmentions.mentions': 'Erwähnungen',
|
'webmentions.mentions': 'Erwähnungen',
|
||||||
|
'pagination.newer': 'Neuere Beiträge',
|
||||||
|
'pagination.older': 'Ältere Beiträge',
|
||||||
|
'pagination.page': 'Seite {current} von {total}',
|
||||||
'lang.de': 'Deutsch',
|
'lang.de': 'Deutsch',
|
||||||
'lang.en': 'English',
|
'lang.en': 'English',
|
||||||
},
|
},
|
||||||
|
|
@ -68,6 +71,9 @@ export const ui = {
|
||||||
'webmentions.reposts': 'Reposts',
|
'webmentions.reposts': 'Reposts',
|
||||||
'webmentions.replies': 'Replies',
|
'webmentions.replies': 'Replies',
|
||||||
'webmentions.mentions': 'Mentions',
|
'webmentions.mentions': 'Mentions',
|
||||||
|
'pagination.newer': 'Newer posts',
|
||||||
|
'pagination.older': 'Older posts',
|
||||||
|
'pagination.page': 'Page {current} of {total}',
|
||||||
'lang.de': 'Deutsch',
|
'lang.de': 'Deutsch',
|
||||||
'lang.en': 'English',
|
'lang.en': 'English',
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
---
|
---
|
||||||
import HomePage from '~/components/HomePage.astro';
|
import PostsList from '~/components/PostsList.astro';
|
||||||
---
|
---
|
||||||
|
|
||||||
<HomePage locale="en" />
|
<PostsList locale="en" />
|
||||||
|
|
|
||||||
18
src/pages/en/page/[page].astro
Normal file
18
src/pages/en/page/[page].astro
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
---
|
||||||
|
import PostsList from '~/components/PostsList.astro';
|
||||||
|
import { getPostsByLocale, POSTS_PER_PAGE } from '~/i18n/posts';
|
||||||
|
|
||||||
|
export async function getStaticPaths() {
|
||||||
|
const posts = await getPostsByLocale('en');
|
||||||
|
const totalPages = Math.max(1, Math.ceil(posts.length / POSTS_PER_PAGE));
|
||||||
|
const paths = [];
|
||||||
|
for (let page = 2; page <= totalPages; page++) {
|
||||||
|
paths.push({ params: { page: String(page) }, props: { page } });
|
||||||
|
}
|
||||||
|
return paths;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { page } = Astro.props;
|
||||||
|
---
|
||||||
|
|
||||||
|
<PostsList locale="en" page={page} />
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
---
|
---
|
||||||
import HomePage from '~/components/HomePage.astro';
|
import PostsList from '~/components/PostsList.astro';
|
||||||
---
|
---
|
||||||
|
|
||||||
<HomePage locale="de" />
|
<PostsList locale="de" />
|
||||||
|
|
|
||||||
18
src/pages/seite/[page].astro
Normal file
18
src/pages/seite/[page].astro
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
---
|
||||||
|
import PostsList from '~/components/PostsList.astro';
|
||||||
|
import { getPostsByLocale, POSTS_PER_PAGE } from '~/i18n/posts';
|
||||||
|
|
||||||
|
export async function getStaticPaths() {
|
||||||
|
const posts = await getPostsByLocale('de');
|
||||||
|
const totalPages = Math.max(1, Math.ceil(posts.length / POSTS_PER_PAGE));
|
||||||
|
const paths = [];
|
||||||
|
for (let page = 2; page <= totalPages; page++) {
|
||||||
|
paths.push({ params: { page: String(page) }, props: { page } });
|
||||||
|
}
|
||||||
|
return paths;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { page } = Astro.props;
|
||||||
|
---
|
||||||
|
|
||||||
|
<PostsList locale="de" page={page} />
|
||||||
|
|
@ -47,6 +47,9 @@ main {
|
||||||
margin: auto;
|
margin: auto;
|
||||||
padding: 3em 1em;
|
padding: 3em 1em;
|
||||||
}
|
}
|
||||||
|
body.home main {
|
||||||
|
width: 1040px;
|
||||||
|
}
|
||||||
h1,
|
h1,
|
||||||
h2,
|
h2,
|
||||||
h3,
|
h3,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue