Compare commits

..

2 commits

Author SHA1 Message Date
Ruben
36a3221dbb Add default templates for list views
Add default language files for English and Norwegian

Add list-card-grid template for card-based content display

Add list-faq template for FAQ-style content display

Add list-grid template for grid-based content display
2025-11-01 16:11:33 +01:00
Ruben
1aa4d6a83b Add modern CSS reset and styling system
Implement responsive grid layout Add CSS variables for consistent
theming Create button component with states Improve header and footer
structure Add page load time measurement Enhance list template with
styling Update template structure with semantic HTML Implement dynamic
CSS file loading Add favicon support Improve navigation with active
state Add page and section class names to body Implement conditional
date display in list items
2025-11-01 16:11:20 +01:00
9 changed files with 883 additions and 53 deletions

274
app/README.md Normal file
View file

@ -0,0 +1,274 @@
# Framework Documentation
This directory contains the core framework for the file-based CMS. It provides a minimal, extensible foundation that can be customized via the `/custom/` directory.
## Architecture
The framework is a lightweight PHP-based routing system that:
- Converts URLs to filesystem paths
- Resolves language-specific content and slugs
- Applies templates to render content
- Serves static assets (styles, fonts, images)
## Directory Structure
```
app/
├── router.php # Main routing logic
├── static.php # Static asset server for /app/* resources
├── config.ini # Default framework configuration
├── vendor/ # Third-party dependencies
│ └── Parsedown/ # Markdown parser
└── default/ # Default templates and styles (fallback)
├── templates/
│ ├── base.php # Base HTML structure
│ ├── page.php # Single page/article template
│ └── list.php # Directory listing template
└── styles/
└── base.css # Default base styles
```
## Core Components
### `router.php`
The main request router that handles all content requests. Key responsibilities:
1. **Language Detection**: Extracts language from URL path (`/no/`, `/en/`)
2. **Slug Resolution**: Converts language-specific slugs to directory names using metadata
3. **Path Resolution**: Maps URLs to filesystem paths in `/content/`
4. **Content Loading**: Reads content files (`.md`, `.html`, `.php`)
5. **Template Application**: Applies appropriate template based on content type
6. **Rendering**: Outputs final HTML with base template
**Key Functions:**
- `resolveLanguageSlugToName()` - Translates slugs to directory names
- `findContentFile()` - Locates content files with language variants
- `renderPage()` - Renders content with templates
- `parseMetadata()` - Parses INI metadata files
### `static.php`
Serves static assets from `/app/` directory. Handles requests for:
- `/app/styles/*` - Framework CSS files
- `/app/fonts/*` - Framework fonts
- `/app/default-styles/*` - Default stylesheet aliases
Includes security checks to prevent path traversal attacks.
### `config.ini`
Default configuration for the framework:
```ini
[languages]
default = "no" # Default language
available = "no,en" # Available languages
```
Can be overridden by `/custom/config.ini`.
### `vendor/`
Third-party dependencies:
- **Parsedown**: Markdown-to-HTML parser library
### `default/`
Fallback templates and styles used when custom templates are not provided.
## Request Flow
```
1. User requests URL (e.g., /no/artikler/pasientinfo)
2. Apache routes to /content/index.php
3. index.php includes router.php
4. router.php:
- Extracts language: "no"
- Resolves slug "artikler" → "artikler" directory
- Resolves slug "pasientinfo" → "pasientinfo" directory
- Checks if path exists in /content/
- Determines content type (file or directory)
5. For single article:
- Loads metadata.ini
- Loads article.no.md (or article.md)
- Applies /custom/templates/page.php (or default)
- Wraps with /custom/templates/base.php
6. For directory listing:
- Scans directory for subdirectories
- Loads metadata.ini for each item
- Applies list template specified in metadata
- Wraps with base.php
7. Outputs final HTML to browser
```
## Template System
Templates can be overridden by placing files in `/custom/templates/`:
### Base Template (`base.php`)
Wraps all content with HTML structure, header, footer, navigation.
**Variables available:**
- `$title` - Page title
- `$content` - Rendered page content
- `$language` - Current language code
- `$metadata` - Page metadata array
### Page Template (`page.php`)
Renders single articles/pages.
**Variables available:**
- `$contentHtml` - Parsed HTML content
- `$metadata` - Article metadata
- `$language` - Current language code
- `$parentMetadata` - Parent directory metadata
### List Templates
Render directory listings. Multiple variants:
- `list.php` - Simple list
- `list-grid.php` - Grid layout
- `list-card-grid.php` - Card grid with images
- `list-faq.php` - Expandable FAQ view
**Variables available:**
- `$items` - Array of child items with metadata
- `$metadata` - Directory metadata
- `$language` - Current language code
## Customization
The framework is designed to be minimal and extensible. All customization should happen in `/custom/`:
### Override Templates
Create files in `/custom/templates/` with the same name as default templates.
### Override Configuration
Create `/custom/config.ini` to override default settings.
### Add Custom Styles
Place CSS in `/custom/styles/` and reference in custom templates.
### Add Custom Fonts
Place font files in `/custom/fonts/` and reference in CSS.
### Add Translations
Create or edit `/custom/languages/[lang].ini` files.
## Metadata System
Content is configured via `metadata.ini` files placed in each directory.
### Common Metadata Fields
```ini
[metadata]
title[no] = "Norwegian Title"
title[en] = "English Title"
slug[no] = "norwegian-slug"
slug[en] = "english-slug"
summary[no] = "Norwegian summary"
summary[en] = "English summary"
date = "2024-10-15"
category = "Category Name"
tags = "tag1,tag2,tag3"
template = "list-card-grid" # For directories
show_in_menu = true
menu_order = 10
```
### Metadata Inheritance
Child items inherit parent metadata when not specified.
## Content File Resolution
The router looks for content files in this order:
1. `article.[language].md` (e.g., `article.no.md`)
2. `article.[language].html`
3. `article.[language].php`
4. `article.md` (fallback)
5. `article.html` (fallback)
6. `article.php` (fallback)
7. `page.[language].md` (for directory index)
8. `page.md` (fallback)
## Language System
### URL Structure
- Norwegian: `/no/artikler/pasientinfo`
- English: `/en/articles/patient-info`
### Slug Translation
Slugs are resolved using metadata:
```ini
slug[no] = "pasientinfo"
slug[en] = "patient-info"
```
The router automatically translates URLs to filesystem paths regardless of language.
### Translation Files
Located in `/custom/languages/`:
- `no.ini` - Norwegian translations
- `en.ini` - English translations
Format:
```ini
key = "Translated text"
another_key = "More text"
```
Access in templates: `$translations['key']`
## Security Considerations
- **Path Traversal Protection**: `static.php` validates paths to prevent directory traversal
- **Input Sanitization**: URLs are sanitized before filesystem operations
- **Template Isolation**: Templates run in controlled scope with specific variables
- **No Database**: File-based system eliminates SQL injection risks
## Performance
- **No Caching**: Content is rendered on each request (suitable for low-traffic sites)
- **Performance Tracking**: Page generation time is measured and displayed
- **Static Assets**: Served directly by Apache when possible
## Extending the Framework
To add new features:
1. **Custom Functions**: Add helper functions in custom templates
2. **New Template Types**: Create new template files in `/custom/templates/`
3. **Metadata Fields**: Add new fields to `metadata.ini` files
4. **Custom Routes**: Extend `router.php` (requires modifying framework)
## Debugging
Enable error reporting in `content/index.php`:
```php
ini_set('display_errors', 1);
error_reporting(E_ALL);
```
View page generation time at bottom of each page.
## Requirements
- PHP 8.3 or higher
- Apache with mod_rewrite enabled
- Write permissions on content directories (for future admin features)
## Limitations
- No built-in caching (regenerates pages on each request)
- No built-in admin interface (content edited via filesystem)
- No user authentication system
- No built-in search functionality
- Performance may degrade with very large content libraries

View file

@ -0,0 +1,12 @@
; English translations
home = "Home"
categories = "Categories"
tags = "Tags"
read_more = "Read more"
read_article = "Read article"
read_full_answer = "Read full answer"
download_pdf = "Download PDF"
summary = "Summary"
footer_text = "Footer content goes here"
footer_handcoded = "This page was generated in"
footer_page_time = "ms"

View file

@ -0,0 +1,12 @@
; Norwegian translations
home = "Hjem"
categories = "Kategorier"
tags = "Stikkord"
read_more = "Les mer"
read_article = "Les artikkel"
read_full_answer = "Les hele svaret"
download_pdf = "Last ned PDF"
summary = "Oppsummering"
footer_text = "Bunntekst her"
footer_handcoded = "Denne siden ble generert på"
footer_page_time = "ms"

View file

@ -1,47 +1,191 @@
/* MINIMAL RESET */
* { margin: 0; padding: 0; box-sizing: border-box; }
/* MINIMAL CSS RESET*/
* { margin-bottom: 0; }
/* GLOBAL */
body {
font-family: system-ui, sans-serif;
line-height: 1.6;
color: #333;
max-width: 800px;
margin: 0 auto;
padding: 1rem;
/* VARIABLES */
:root {
--font-body: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
--font-heading: Georgia, "Times New Roman", serif;
--color-primary: #4a90e2;
--color-primary: oklch(0.65 0.15 250);
--color-secondary: #2c5aa0;
--color-secondary: oklch(0.50 0.12 250);
--color-light: #f0f4f8;
--color-light: oklch(0.97 0.01 250);
--color-grey: #404040;
--color-grey: oklch(0.37 0 0);
}
/* GLOBAL */
html { font-family: var(--font-body); font-size: clamp(16px, 2.3vw, 20px); scroll-behavior: smooth; }
body { margin: 0; color: var(--color-grey) }
p, ul, ol, aside { line-height: 1.5em; hyphens: auto }
img { max-width: 100%; height: auto; }
h1 { color: var(--color-primary); font-size: 2.3rem }
h1, h2, h3, h4, h5, h6 {
font-family: var(--font-heading);
font-weight: 400;
line-height: 1.3em;
margin-top: 1.3em;
text-wrap: pretty;
}
a {
color: var(--color-primary);
text-decoration: none;
&:hover { color: var(--color-secondary) }
}
a { color: #0066cc; text-decoration: none; }
a:hover { text-decoration: underline; }
.grid-container {
display: grid;
grid-template-rows: auto 1fr auto;
grid-template-columns: 1fr;
grid-template-areas: "header" "main" "footer";
height: 100%;
width: 100%;
justify-content: center;
min-height: 100vh;
align-items: stretch;
}
.contain, :where(main>article, main>aside, main>section) {
display: grid;
grid-template-columns: minmax(.4rem, 1fr) minmax(0, 42rem) minmax(.3rem, 1fr);
> * {
grid-column: 2;
}
}
.escape {
grid-column: 1 / -1 !important;
}
/* HEADER */
header {
border-bottom: 2px solid #eee;
padding-bottom: 1rem;
margin-bottom: 2rem;
border-bottom: 3px #00000022 solid;
grid-area: header;
> div {
padding-bottom: .2rem;
display: flex;
.logo {
margin-right: .3rem;
svg {
width: 7rem;
height: 100%;
color: var(--color-primary);
}
}
header h1 { font-size: 1.5rem; }
nav {
display:flex;
align-items: center;
justify-content:flex-end;
flex: 1;
ul {
display: flex;
list-style: none;
flex-wrap: wrap;
margin-top: .4rem;
padding: 0;
justify-content: flex-end;
a {
margin-left:0.4rem;
margin-top:0.4rem;
}
}
}
}
}
/* MAIN */
main { margin-bottom: 2rem; }
main {
grid-area: main;
background-color: var(--color-light);
padding-bottom: 2rem;
article { margin-bottom: 2rem; }
h1 {
font-size: 1.8rem;
margin-bottom: 0.5rem;
aside { margin-top: 1.3em }
article {
.intro {
font-size: 1.2rem;
line-height: 1.35em;
}
}
.button {
margin-top: 1.3rem;
justify-self: start;
}
}
p { margin-bottom: 1rem; }
/* BUTTONS */
.button {
display: inline-block;
text-decoration: none;
border-radius: 2rem;
padding: 0.35rem 1rem;
background-color: transparent;
color: var(--color-grey);
outline: 0.08rem var(--color-grey) solid;
&:hover {
background-color: var(--color-grey);
color: white;
outline: none;
}
&:active, &.active {
background-color: var(--color-primary);
color: white;
outline: none;
}
&:focus {
background-color: var(--color-primary);
color: white;
outline: none;
}
&.inverted {
background-color: transparent;
color: white;
outline: 0.08rem white solid;
&:hover {
background-color: white;
color: var(--color-primary);
outline: none;
}
&:active, &.active {
background-color: var(--color-light);
color: var(--color-primary);
outline: none;
}
&:focus {
color: white;
background-color: var(--color-grey);
outline: none;
}
}
&.bigger {
font-size: 1.2em;
padding: calc(0.35rem * 1.2) calc(1rem * 1.2);
border-radius: calc(1rem * 1.2);
}
}
/* FOOTER */
footer {
border-top: 2px solid #eee;
padding-top: 1rem;
text-align: center;
font-size: 0.9rem;
color: #666;
color: var(--color-light);
a {
color: var(--color-light);
&:hover { color: white; text-decoration: underline }
}
background-color: var(--color-secondary);
grid-area: footer;
> div {
margin: 1rem 0;
text-align: center;
.generated { font-size: .6rem }
}
}

View file

@ -1,29 +1,63 @@
<?php
$startTime = microtime(true);
$publicDir = realpath($_SERVER['DOCUMENT_ROOT']);
$customCssPath = __DIR__ . '/../../custom/styles/base.css';
$defaultCssPath = __DIR__ . '/../styles/base.css';
$cssPath = file_exists($customCssPath) ? $customCssPath : $defaultCssPath;
$cssUrl = file_exists($customCssPath) ? '/app/styles/base.css' : '/app/default-styles/base.css';
$cssHash = file_exists($cssPath) ? hash_file('md5', $cssPath) : 'file_not_found';
if (isset($GLOBALS['_SERVER']['SCRIPT_FILENAME'])) { $includingFile = $_SERVER['SCRIPT_FILENAME']; }
if (!empty($includingFile)) { $pageName = pathinfo($includingFile, PATHINFO_FILENAME); }
if (!in_array(basename(dirname($includingFile)), ['latest', 'live', 'frozen']) && basename(dirname($includingFile)) !== '') { $dirName = basename(dirname($includingFile)); }
function getActiveClass($href) { return rtrim(parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH), '/') === rtrim($href, '/') ? 'active' : ''; }
?>
<!DOCTYPE html>
<html lang="<?= $currentLang ?? 'en' ?>">
<html lang="<?= htmlspecialchars($currentLang ?? 'en') ?>">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="<?= file_exists(dirname(dirname(__DIR__)) . '/custom/styles/base.css') ? '/app/styles/base.css' : '/app/default-styles/base.css' ?>">
<title><?= htmlspecialchars($pageTitle ?? 'Site') ?></title>
<link rel="stylesheet" href="<?= $cssUrl ?>?v=<?= $cssHash ?>">
<link rel="icon" href="/favicon.svg" type="image/svg+xml">
<link rel="icon" href="/favicon.png" type="image/png">
<title><?= htmlspecialchars($pageTitle ?? 'Site Title') ?></title>
</head>
<body>
<header>
<h1><a href="/">Webfolder demo</a></h1>
<?php if (!empty($navigation)): ?>
<body class="<?php if (isset($dirName)) echo 'section-' . $dirName . ' '; ?><?php if (isset($pageName)) echo 'page-' . $pageName; ?>">
<div class="grid-container">
<header class="contain">
<div>
<div class="logo">
<a href="/">
<svg width="200" height="60" viewBox="0 0 200 60" xmlns="http://www.w3.org/2000/svg">
<text x="10" y="40" font-family="Arial, sans-serif" font-size="32" font-weight="bold" fill="currentColor">LOGO</text>
</svg>
</a>
</div>
<nav>
<ul>
<a href="/" class="button <?php echo getActiveClass('/'); ?>"><li><?= htmlspecialchars($translations['home'] ?? 'Home') ?></li></a>
<?php if (!empty($navigation)): ?>
<?php foreach ($navigation as $item): ?>
<li><a href="<?= htmlspecialchars($item['url']) ?>"><?= htmlspecialchars($item['title']) ?></a></li>
<a href="<?= htmlspecialchars($item['url']) ?>" class="button <?php echo getActiveClass($item['url']); ?>"><li><?= htmlspecialchars($item['title']) ?></li></a>
<?php endforeach; ?>
<?php endif; ?>
</ul>
</nav>
<?php endif; ?>
</div>
</header>
<main>
<?= $content ?>
<?php echo $content ?? ''; ?>
</main>
<footer>
<p>&copy; <?= date('Y') ?> <?= htmlspecialchars($translations['footer_copyright'] ?? '') ?></p>
<div class="contain">
<p><?= htmlspecialchars($translations['footer_text'] ?? 'Footer content goes here') ?></p>
<?php $endTime = microtime(true); $pageLoadTime = round(($endTime - $startTime) * 1000, 2); ?>
<p class="generated"><?= htmlspecialchars($translations['footer_handcoded'] ?? 'This page was generated in') ?> <?php echo $pageLoadTime; ?><?= htmlspecialchars($translations['footer_page_time'] ?? 'ms') ?></p>
</div>
</footer>
</div>
</body>
</html>

View file

@ -0,0 +1,82 @@
<?php if (!empty($pageContent)): ?>
<article class="list-intro">
<?= $pageContent ?>
</article>
<?php endif; ?>
<section class="list-card-grid-wrapper">
<div class="list-card-grid">
<?php foreach ($items as $item): ?>
<article>
<?php if ($item['cover']): ?>
<a href="<?= htmlspecialchars($item['pdf'] ?? $item['url']) ?>">
<img src="<?= htmlspecialchars($item['cover']) ?>" alt="<?= htmlspecialchars($item['title']) ?>">
</a>
<?php endif; ?>
<h1>
<a href="<?= htmlspecialchars($item['redirect'] ?? $item['url']) ?>">
<?= htmlspecialchars($item['title']) ?>
</a>
</h1>
<?php if (($metadata['show_date'] ?? true) && !empty($item['date'])): ?>
<p><?= htmlspecialchars($item['date']) ?></p>
<?php endif; ?>
<?php if ($item['summary']): ?>
<p><?= htmlspecialchars($item['summary']) ?></p>
<?php endif; ?>
<div class="card-actions">
<?php if (!empty($item['pdf'])): ?>
<a href="<?= htmlspecialchars($item['pdf']) ?>" class="button" download><?= htmlspecialchars($translations['download_pdf'] ?? 'Download PDF') ?></a>
<?php endif; ?>
<?php if (!empty($item['redirect'])): ?>
<a href="<?= htmlspecialchars($item['redirect']) ?>" class="button"><?= htmlspecialchars($translations['read_article'] ?? 'Read article') ?></a>
<?php else: ?>
<a href="<?= htmlspecialchars($item['url']) ?>" class="button"><?= htmlspecialchars($translations['read_more'] ?? 'Read more') ?></a>
<?php endif; ?>
</div>
</article>
<?php endforeach; ?>
</div>
</section>
<style>
main > section.list-card-grid-wrapper {
margin-top: 1.3em;
.list-card-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(clamp(15rem, 45%, 20rem), 1fr));
gap: clamp(1rem, 3vw, 2rem);
}
.list-card-grid > article {
background-color: white;
padding: 0;
overflow: hidden;
display: flex;
flex-direction: column;
> :not(img, a) {
padding-left: 1rem;
padding-right: 1rem;
}
h1 {
margin-top: 1rem;
font-size: clamp(1.5rem, 4vw, 2rem);
}
.card-actions {
margin-top: auto;
display: flex;
gap: 0.3rem;
flex-wrap: wrap;
padding-bottom: 1rem;
a {
margin-top: 1.3rem;
}
}
}
}
</style>

View file

@ -0,0 +1,141 @@
<?php if (!empty($pageContent)): ?>
<article class="list-intro">
<?= $pageContent ?>
</article>
<?php endif; ?>
<section class="list-faq-wrapper">
<div class="list-faq">
<?php foreach ($items as $item): ?>
<details class="faq-item">
<summary>
<h2><?= htmlspecialchars($item['title']) ?></h2>
<span class="toggle-icon" aria-hidden="true"></span>
</summary>
<div class="faq-content">
<?php if ($item['summary']): ?>
<p><strong><?= htmlspecialchars($translations['summary'] ?? 'Summary') ?>:</strong> <?= htmlspecialchars($item['summary']) ?></p>
<?php endif; ?>
<a href="<?= htmlspecialchars($item['url']) ?>" class="button"><?= htmlspecialchars($translations['read_full_answer'] ?? 'Read full answer') ?></a>
</div>
</details>
<?php endforeach; ?>
</div>
</section>
<style>
main > section.list-faq-wrapper {
margin-top: 1.3em;
.list-faq {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.faq-item {
background-color: white;
border: 1px solid #e0e0e0;
border-radius: 0.5rem;
overflow: hidden;
transition: border-color 0.2s ease;
&:hover {
border-color: var(--color-primary);
}
&[open] {
border-color: var(--color-primary);
}
}
summary {
display: flex;
align-items: center;
justify-content: space-between;
padding: 1rem 1.5rem;
cursor: pointer;
list-style: none;
user-select: none;
&::-webkit-details-marker {
display: none;
}
h2 {
margin: 0;
font-size: 1.3rem;
font-weight: 400;
color: var(--color-grey);
flex: 1;
}
.toggle-icon {
width: 1.5rem;
height: 1.5rem;
flex-shrink: 0;
position: relative;
margin-left: 1rem;
&::before,
&::after {
content: '';
position: absolute;
background-color: var(--color-primary);
transition: transform 0.3s ease;
}
&::before {
top: 50%;
left: 0;
right: 0;
height: 2px;
transform: translateY(-50%);
}
&::after {
left: 50%;
top: 0;
bottom: 0;
width: 2px;
transform: translateX(-50%);
}
}
&:hover h2 {
color: var(--color-primary);
}
}
.faq-item[open] summary .toggle-icon::after {
transform: translateX(-50%) rotate(90deg);
opacity: 0;
}
.faq-content {
padding: 0 1.5rem 1.5rem 1.5rem;
animation: slideDown 0.3s ease;
p {
margin-top: 0;
color: var(--color-grey);
line-height: 1.6;
}
.button {
margin-top: 1rem;
}
}
@keyframes slideDown {
from {
opacity: 0;
transform: translateY(-0.5rem);
}
to {
opacity: 1;
transform: translateY(0);
}
}
}
</style>

View file

@ -0,0 +1,94 @@
<?php if (!empty($pageContent)): ?>
<article class="list-intro">
<?= $pageContent ?>
</article>
<?php endif; ?>
<section class="list-grid-wrapper">
<div class="list-grid">
<?php foreach ($items as $item): ?>
<article>
<?php if ($item['cover']): ?>
<a href="<?= htmlspecialchars($item['url']) ?>">
<img src="<?= htmlspecialchars($item['cover']) ?>" alt="<?= htmlspecialchars($item['title']) ?>">
</a>
<?php endif; ?>
<h1>
<a href="<?= htmlspecialchars($item['url']) ?>">
<?= htmlspecialchars($item['title']) ?>
</a>
</h1>
<?php if (($metadata['show_date'] ?? true) && !empty($item['date'])): ?>
<p><?= htmlspecialchars($item['date']) ?></p>
<?php endif; ?>
<?php if ($item['summary']): ?>
<p><?= htmlspecialchars($item['summary']) ?></p>
<?php endif; ?>
<div class="grid-actions">
<?php if (!empty($item['pdf'])): ?>
<a href="<?= htmlspecialchars($item['pdf']) ?>" class="button" download><?= htmlspecialchars($translations['download_pdf'] ?? 'Download PDF') ?></a>
<?php endif; ?>
<a href="<?= htmlspecialchars($item['url']) ?>" class="button"><?= htmlspecialchars($translations['read_more'] ?? 'Read more') ?></a>
</div>
</article>
<?php endforeach; ?>
</div>
</section>
<style>
main > section.list-grid-wrapper {
margin-top: 1.3em;
.list-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(clamp(15rem, 45%, 20rem), 1fr));
gap: clamp(1rem, 3vw, 2rem);
}
.list-grid > article {
background-color: white;
padding: 0;
overflow: hidden;
display: flex;
flex-direction: column;
> :not(img, a) {
padding-left: 1rem;
padding-right: 1rem;
}
h1 {
margin-top: 1rem;
font-size: clamp(1.5rem, 4vw, 2rem);
}
.grid-actions {
margin-top: auto;
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
padding-bottom: 1rem;
a {
margin-top: 1.3rem;
border-radius: 1rem;
background-color: var(--color-grey);
padding: 0.35rem 1rem;
color: white;
text-decoration: none;
&:hover {
background-color: var(--color-primary);
color: white;
}
&:focus {
outline: 0.1rem var(--color-primary) solid;
color: var(--color-grey);
background-color: white;
}
}
}
}
}
</style>

View file

@ -1,18 +1,55 @@
<?php if (!empty($pageContent)): ?>
<article class="list-intro">
<?= $pageContent ?>
</article>
<?php endif; ?>
<article>
<?php foreach ($items as $item): ?>
<article>
<?php if ($item['cover']): ?>
<a href="<?= htmlspecialchars($item['url']) ?>">
<img src="<?= htmlspecialchars($item['cover']) ?>" alt="<?= htmlspecialchars($item['title']) ?>">
</a>
<?php endif; ?>
<h1>
<a href="<?= htmlspecialchars($item['url']) ?>">
<?= htmlspecialchars($item['title']) ?>
</a>
</h1>
<?php if (($metadata['show_date'] ?? true) && !empty($item['date'])): ?>
<p><?= htmlspecialchars($item['date']) ?></p>
<?php endif; ?>
<?php if ($item['summary']): ?>
<p><?= htmlspecialchars($item['summary']) ?></p>
<?php endif; ?>
<a href="<?= htmlspecialchars($item['url']) ?>" class="button"><?= htmlspecialchars($translations['read_more'] ?? 'Read more') ?></a>
</article>
<?php endforeach; ?>
</article>
<style>
main > article {
> article {
background-color: white;
padding: 0;
padding-bottom: 1.3rem;
margin-bottom: 1.5rem;
overflow: hidden;
> :not(img, a) {
padding-left: 1rem;
padding-right: 1rem;
}
h1 {
margin-top: 1rem;
}
> .button {
margin-left: 1rem;
margin-top: 1rem;
}
}
}
</style>