Compare commits

..
3 Commits
Author SHA1 Message Date
m038andClaude Opus 5 1b9e51baf7 Turn off travelling so home shows the between-trips state everywhere
travelling was only set to false in user/env/localhost/config/site.yaml,
which Grav reads only when the request hostname is literally "localhost",
and which is git-ignored. So the home page still rendered Denmark 2026 as
an active trip on every other hostname, and the setting could never reach
production. Set it in the tracked config instead.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-29 23:12:53 +02:00
m038andClaude Opus 5 e81c73a9ba Fix story scrolly text rendering; story/trip blueprint media upload
Scrolly-section text never rendered: the inline Scrollama block ran before
the main.js bundle published window.scrollama, so it always hit its guard
and the panel text stayed display:none, leaving a blank column beside the
pinned image. Defer to DOMContentLoaded, and build the step panels before
the scrollama check so a missing bundle costs the animation, not the text.

Also fixes an invalid calc(-(...)) that dropped the mobile scrolly offset,
and drops the duplicate inline back-to-top in favour of main.js's version
(which pushes a history entry).

Blueprint changes carried over from an earlier session: story.yaml and
trip.yaml now extend the default page form so both can upload media.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-29 23:12:45 +02:00
m038andClaude Opus 5 0ebb770bc7 Add Vestkustryten reflection story; correct Denmark 2026 trip facts
New story for the Denmark 2026 trip with 9 photos, plus trip.md updates:
route length 560 → 650 km and end date 2026-07-20 → 2026-07-18.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-29 23:07:03 +02:00
16 changed files with 156 additions and 52 deletions
+1 -1
View File
@@ -6,5 +6,5 @@ metadata:
description: 'Into the East — travel journal' description: 'Into the East — travel journal'
description: 'A travel blog by Mischa' description: 'A travel blog by Mischa'
active_trip: /trips/denmark-2026 active_trip: /trips/denmark-2026
travelling: true travelling: false
owner_username: mischa owner_username: mischa
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 MiB

@@ -0,0 +1,49 @@
---
force_connect: false
published: true
featured: true
title: 'Vestkustryten - a reflection'
date: '29-07-2026 19:51'
end_date: '29-07-2026 19:51'
transport_mode: bicycle
hero_image: promenade-and-bike.jpg
hero_alt: 'Hidden sign of Vestkustryten 1 in the dune grass'
---
A week and a half after my last day in the saddle, I'm ready to face my own commitment: one journal entry per day, one story per trip. Instead of a highlight, though, a reflection feels more fitting. Why? Because this trip was more than a holiday. It was a confirmation that I actually enjoy cycling — and the sum of every trip that came before it, including the ones I didn't always enjoy.
[chapter-break image="gravel-road-through-dunes-bw.jpg" title="Rookie mistakes" number="I" alt="" /]
[scrolly-section image="bike-on-the-beach.jpg" alt=""]
BerlinPrague: too challenging for a rookie. BerlinCopenhagen: an accident brought trouble. A roundtrip in Tuscany: nine days of hills that nearly killed me. Vestkystruten 2026: a grand success, built entirely on those past failures and everything they taught me.
---
When you fall off your bike, you get up and ride again. And believe me — I know what I'm talking about when it comes to falling ;)
[/scrolly-section]
[chapter-break image="route-coast-straight.jpg" title="Riding Free" number="3" alt="" /]
My goal was to cycle free: no strict daily plan, just listening to my body and my surroundings. What I learned is that cycling is fun the moment you allow yourself to enjoy it — to take the break you need, eat the snack your body's asking for, and let the path be the point instead of the destination. Once you spend several days travelling under your own power, you realise it isn't like a flight or a train. Those get you to somewhere. The bike is a companion that carries you along the way to whatever it is you're looking for.
[chapter-break image="blue-clouds-on-beach.jpg" title="Everything You Miss" number="2" alt="" /]
[scrolly-section image="beautiful-dune-grass.jpg" alt=""]
I should have stopped more often in the cute little towns. I should have paused longer to soak up the nature. I could have spent more time swimming in the sea. These are all thoughts that surfaced while riding, or afterwards, reflecting at the end of a day in the saddle.
---
All valid — and all something to make peace with. You'll always miss a lovely little spot or the next cool thing. But everything you miss because you were somewhere else is also space you opened up for a different experience, another adventure.
[/scrolly-section]
[scrolly-section image="crazy-eyes.jpg" alt="Crazy eyes"]
About 650 kilometers in 8 days, and I look back with pride — not at the distance, but at how far I've come since those earlier trips, all of it adding up to an amazing holiday in Denmark. That's it. Denmark is awesome, my bike is awesome, and I can't wait for wherever the next one takes me.
[/scrolly-section]
[snap-gallery images="sea-grass-sign-route-1.jpg" captions="Vestkystruten - check" /]
+2 -2
View File
@@ -3,9 +3,9 @@ title: 'Vestkustryten - Denmark 2026'
template: trip template: trip
date: '2026-07-10' date: '2026-07-10'
date_start: '2026-07-10' date_start: '2026-07-10'
date_end: '2026-07-20' date_end: '2026-07-18'
cover_image: bikepacking_denmark.jpeg cover_image: bikepacking_denmark.jpeg
tagline: "560 km up Denmark's wild North Sea coast to where two seas meet" tagline: "650 km up Denmark's wild North Sea coast to where two seas meet"
published: true published: true
autoconnect: intelligent_gpx autoconnect: intelligent_gpx
--- ---
+64 -12
View File
@@ -1,5 +1,12 @@
title: 'Story' title: 'Story'
# Inherits Grav's default page form — this is what supplies the page-media
# uploader, plus the standard Options and Advanced tabs. Without it the form is
# standalone and has no way to upload images (see entry.yaml, same pattern).
'@extends':
type: default
context: blueprints://pages
form: form:
fields: fields:
tabs: tabs:
@@ -8,6 +15,7 @@ form:
fields: fields:
content: content:
ordering@: 0
type: tab type: tab
title: Content title: Content
fields: fields:
@@ -34,7 +42,7 @@ form:
type: text type: text
label: Hero Image label: Hero Image
placeholder: 'hero.jpg' placeholder: 'hero.jpg'
help: 'Filename of the hero image (upload via Media tab)' help: 'Filename of an image uploaded in the Images field below'
header.hero_alt: header.hero_alt:
type: text type: text
@@ -44,10 +52,53 @@ form:
content: content:
type: markdown type: markdown
label: Content label: Content
help: 'Prose renders as a normal reading column. For layout blocks (scrolly sections, galleries, pull quotes) use the shortcodes listed on the Blocks tab.'
validate: validate:
required: true required: true
header.media_order:
type: pagemedia
label: Images
help: 'Upload every image the story needs here first — shortcodes refer to them by bare filename (photo-1.jpg), never by path.'
blocks:
ordering@: 1
type: tab
title: Blocks
fields:
blocks_intro:
type: spacer
text: "<p>Paste these into the <strong>Content</strong> field. Image parameters take a <strong>bare filename</strong> of an image uploaded in the <strong>Images</strong> field on the Content tab. The hero image is not a block \u2014 it has its own field on the Content tab.</p>"
blocks_scrolly:
type: spacer
text: "<p style=\"margin:1.4em 0 0.4em\"><strong>Scrolly section \u2014 text panels scroll over a pinned image</strong></p><pre style=\"white-space:pre-wrap;word-break:break-word;padding:0.6em 0.8em;border-radius:6px;background:rgba(127,127,127,0.12)\">[scrolly-section image=\"hero.jpg\" alt=\"What the image shows\" caption=\"Optional caption\"]\nFirst panel.\n\n---\n\nSecond panel. A --- starts a new panel.\n[/scrolly-section]</pre>"
blocks_pullquote:
type: spacer
text: "<p style=\"margin:1.4em 0 0.4em\"><strong>Pull quote \u2014 large extracted quote, image optional</strong></p><pre style=\"white-space:pre-wrap;word-break:break-word;padding:0.6em 0.8em;border-radius:6px;background:rgba(127,127,127,0.12)\">[pull-quote image=\"photo-1.jpg\" alt=\"What the image shows\"]\nThe quote itself. Markdown works in here.\n[/pull-quote]</pre><p>Drop <em>image</em> and <em>alt</em> for the plain no-image variant.</p>"
blocks_chapter:
type: spacer
text: "<p style=\"margin:1.4em 0 0.4em\"><strong>Chapter break \u2014 section transition over a background image</strong></p><pre style=\"white-space:pre-wrap;word-break:break-word;padding:0.6em 0.8em;border-radius:6px;background:rgba(127,127,127,0.12)\">[chapter-break image=\"photo-1.jpg\" title=\"After Dark\" number=\"II\" alt=\"What the image shows\" /]</pre><p><em>number</em> is optional.</p>"
blocks_gallery:
type: spacer
text: "<p style=\"margin:1.4em 0 0.4em\"><strong>Snap gallery \u2014 swipeable carousel</strong></p><pre style=\"white-space:pre-wrap;word-break:break-word;padding:0.6em 0.8em;border-radius:6px;background:rgba(127,127,127,0.12)\">[snap-gallery images=\"hero.jpg,photo-1.jpg\" captions=\"First caption,Second caption\" alts=\"First alt,Second alt\" /]</pre><p><strong>No commas inside captions.</strong> The three lists are split on commas and zipped by position, so one comma in a caption shifts every caption after it. Use em dashes or semicolons.</p>"
blocks_fullbleed:
type: spacer
text: "<p style=\"margin:1.4em 0 0.4em\"><strong>Full bleed \u2014 single edge-to-edge image as a visual pause</strong></p><pre style=\"white-space:pre-wrap;word-break:break-word;padding:0.6em 0.8em;border-radius:6px;background:rgba(127,127,127,0.12)\">[full-bleed image=\"photo-2.jpg\" alt=\"What the image shows\" caption=\"Optional\" credit=\"Optional\" /]</pre>"
blocks_imagecaption:
type: spacer
text: "<p style=\"margin:1.4em 0 0.4em\"><strong>Image with caption \u2014 photo at a chosen width</strong></p><pre style=\"white-space:pre-wrap;word-break:break-word;padding:0.6em 0.8em;border-radius:6px;background:rgba(127,127,127,0.12)\">[image-caption image=\"photo-2.jpg\" alt=\"What the image shows\" caption=\"Optional\" credit=\"Optional\" width=\"column\" /]</pre><p><em>width</em>: column (default), full, or bleed.</p>"
blocks_gotchas:
type: spacer
text: "<p style=\"margin:1.4em 0 0.4em\"><strong>Two things that bite</strong></p><p>Self-closing tags need the space before <code>/]</code>. And a mistyped shortcode fails silently \u2014 it renders as literal text or vanishes, with no warning. Preview the page after adding each block.</p>"
location: location:
ordering@: 2
type: tab type: tab
title: Location title: Location
fields: fields:
@@ -97,21 +148,13 @@ form:
validate: validate:
type: bool type: bool
# Published now comes from the inherited Options tab — defining it here
# too would render two toggles bound to the same header key.
publishing: publishing:
ordering@: 3
type: tab type: tab
title: Publishing title: Publishing
fields: fields:
header.published:
type: toggle
label: Published
highlight: 1
default: 1
options:
1: 'Yes'
0: 'No'
validate:
type: bool
header.featured: header.featured:
type: toggle type: toggle
label: Featured highlight label: Featured highlight
@@ -123,3 +166,12 @@ form:
0: 'No' 0: 'No'
validate: validate:
type: bool type: bool
# Drop the inherited Date field — Story defines its own required
# "Start Date" on the Content tab, and both bind to header.date.
options:
fields:
publishing:
fields:
header.date:
unset@: true
+20 -14
View File
@@ -1,5 +1,13 @@
title: 'Trip' title: 'Trip'
# Inherits Grav's default page form — this is what supplies the page-media
# uploader (used for cover images and GPX files), plus the standard Options and
# Advanced tabs. Without it the form is standalone and can only *select*
# existing media, never upload any.
'@extends':
type: default
context: blueprints://pages
form: form:
fields: fields:
tabs: tabs:
@@ -8,6 +16,7 @@ form:
fields: fields:
content: content:
ordering@: 0
type: tab type: tab
title: Content title: Content
fields: fields:
@@ -21,7 +30,15 @@ form:
type: markdown type: markdown
label: Description label: Description
# Inherited from the default blueprint; relabelled because this is
# also where GPX files and cover images get uploaded.
header.media_order:
type: pagemedia
label: Images and GPX files
help: 'Upload cover images and .gpx route files here. GPX tracks are auto-detected on the map — no manual linking. The Cover Image field below picks from what is uploaded here.'
trip: trip:
ordering@: 1
type: tab type: tab
title: Trip title: Trip
fields: fields:
@@ -78,17 +95,6 @@ form:
'manual': 'Manual — force connect only' 'manual': 'Manual — force connect only'
'intelligent_gpx': 'Intelligent GPX — suppress where route is covered' 'intelligent_gpx': 'Intelligent GPX — suppress where route is covered'
publishing: # No Publishing tab — it held only the Published toggle, which now comes
type: tab # from the inherited Options tab. Defining it here too would render two
title: Publishing # toggles bound to the same header key.
fields:
header.published:
type: toggle
label: Published
highlight: 1
default: 1
options:
1: 'Yes'
0: 'No'
validate:
type: bool
+1 -1
View File
@@ -2086,7 +2086,7 @@ body::after {
.scrolly-step:last-child { padding-bottom: 50vh; } .scrolly-step:last-child { padding-bottom: 50vh; }
@media (max-width: 768px), (pointer: coarse) { @media (max-width: 768px), (pointer: coarse) {
.scrolly { display: block; } .scrolly { display: block; }
.scrolly__steps { margin-top: calc(-(100vh - var(--site-header-height))); position: relative; z-index: 1; } .scrolly__steps { margin-top: calc(-1 * (100vh - var(--site-header-height))); position: relative; z-index: 1; }
.scrolly-step { min-height: 80vh; padding: var(--space-8) var(--space-6); align-items: center; justify-content: center; } .scrolly-step { min-height: 80vh; padding: var(--space-8) var(--space-6); align-items: center; justify-content: center; }
.scrolly-step:last-child { padding-bottom: 50vh; } .scrolly-step:last-child { padding-bottom: 50vh; }
} }
+19 -22
View File
@@ -132,23 +132,11 @@
update(); update();
})(); })();
/* ── Back to top button ─────────────────────────────────── */ /* ── Back to top button ───────────────────────────────────
(function () { Owned by initBackToTop('story-totop') in js/src/main.js, called from its
var btn = document.getElementById('story-totop'); DOMContentLoaded boot. An inline copy used to live here and double-bound
if (!btn) return; both the click and scroll handlers; it also lacked the history.pushState
var threshold = window.innerHeight * 0.8; that makes Back return to the reading position. Do not re-add it. */
var shown = false;
btn.addEventListener('click', function () {
window.scrollTo({ top: 0, behavior: 'smooth' });
});
window.addEventListener('scroll', function () {
var shouldShow = window.scrollY > threshold;
if (shouldShow !== shown) {
shown = shouldShow;
btn.classList.toggle('is-visible', shown);
}
}, { passive: true });
})();
/* ── ChapterBreak scroll-reveal ─────────────────────────── */ /* ── ChapterBreak scroll-reveal ─────────────────────────── */
(function () { (function () {
@@ -216,11 +204,17 @@
update(); update();
})(); })();
/* ── ScrollySection (Scrollama) ──────────────────────────── */ /* ── ScrollySection (Scrollama) ────────────────────────────
(function () { Deferred to DOMContentLoaded on purpose: `scrollama` is published by the
js/main.js bundle, which Grav emits via assets.js('bottom') *after* this
inline script (base.html.twig renders block content first). Running inline
would hit the scrollama guard every time, leaving .scrolly__steps-content
hidden and the text column blank. DOMContentLoaded fires only after all
classic scripts have executed, so scrollama is available by then. */
document.addEventListener('DOMContentLoaded', function () {
var reduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches; var reduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
var sections = document.querySelectorAll('.scrolly'); var sections = document.querySelectorAll('.scrolly');
if (!sections.length || typeof scrollama === 'undefined') return; if (!sections.length) return;
var panOffsets = ['50% 40%', '50% 50%', '50% 60%', '50% 45%', '50% 55%']; var panOffsets = ['50% 40%', '50% 50%', '50% 60%', '50% 45%', '50% 55%'];
@@ -252,7 +246,10 @@
stepsWrap.appendChild(step); stepsWrap.appendChild(step);
}); });
if (reduced) { /* Step-building above must never be gated on scrollama: the slot content is
display:none in CSS, so bailing before this point loses the text entirely.
Degrade to plain visible panels instead — no pinning, but readable. */
if (reduced || typeof scrollama === 'undefined') {
section.querySelectorAll('.scrolly-step').forEach(function (s) { s.classList.add('is-active'); }); section.querySelectorAll('.scrolly-step').forEach(function (s) { s.classList.add('is-active'); });
return; return;
} }
@@ -281,6 +278,6 @@
if (d.direction === 'up') d.element.classList.remove('is-active'); if (d.direction === 'up') d.element.classList.remove('is-active');
}); });
}); });
})(); });
</script> </script>
{% endblock %} {% endblock %}