Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1d4bf4e929 | ||
|
|
1b9e51baf7 | ||
|
|
e81c73a9ba | ||
|
|
0ebb770bc7 |
@@ -6,5 +6,5 @@ metadata:
|
||||
description: 'Into the East — travel journal'
|
||||
description: 'A travel blog by Mischa'
|
||||
active_trip: /trips/denmark-2026
|
||||
travelling: true
|
||||
travelling: false
|
||||
owner_username: mischa
|
||||
|
||||
|
After Width: | Height: | Size: 3.3 MiB |
|
After Width: | Height: | Size: 6.5 MiB |
|
After Width: | Height: | Size: 2.2 MiB |
|
After Width: | Height: | Size: 1.0 MiB |
|
After Width: | Height: | Size: 2.5 MiB |
|
After Width: | Height: | Size: 1.8 MiB |
|
After Width: | Height: | Size: 3.4 MiB |
|
After Width: | Height: | Size: 6.5 MiB |
|
After Width: | Height: | Size: 4.7 MiB |
@@ -0,0 +1,49 @@
|
||||
---
|
||||
force_connect: false
|
||||
published: true
|
||||
featured: true
|
||||
title: 'Vestkystruten - 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=""]
|
||||
|
||||
Berlin–Prague: too challenging for a rookie. Berlin–Copenhagen: 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" /]
|
||||
@@ -1,11 +1,11 @@
|
||||
---
|
||||
title: 'Vestkustryten - Denmark 2026'
|
||||
title: 'Vestkystruten - Denmark 2026'
|
||||
template: trip
|
||||
date: '2026-07-10'
|
||||
date_start: '2026-07-10'
|
||||
date_end: '2026-07-20'
|
||||
date_end: '2026-07-18'
|
||||
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
|
||||
autoconnect: intelligent_gpx
|
||||
---
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
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:
|
||||
fields:
|
||||
tabs:
|
||||
@@ -8,6 +15,7 @@ form:
|
||||
fields:
|
||||
|
||||
content:
|
||||
ordering@: 0
|
||||
type: tab
|
||||
title: Content
|
||||
fields:
|
||||
@@ -34,7 +42,7 @@ form:
|
||||
type: text
|
||||
label: Hero Image
|
||||
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:
|
||||
type: text
|
||||
@@ -44,10 +52,53 @@ form:
|
||||
content:
|
||||
type: markdown
|
||||
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:
|
||||
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:
|
||||
ordering@: 2
|
||||
type: tab
|
||||
title: Location
|
||||
fields:
|
||||
@@ -97,21 +148,13 @@ form:
|
||||
validate:
|
||||
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:
|
||||
ordering@: 3
|
||||
type: tab
|
||||
title: Publishing
|
||||
fields:
|
||||
header.published:
|
||||
type: toggle
|
||||
label: Published
|
||||
highlight: 1
|
||||
default: 1
|
||||
options:
|
||||
1: 'Yes'
|
||||
0: 'No'
|
||||
validate:
|
||||
type: bool
|
||||
|
||||
header.featured:
|
||||
type: toggle
|
||||
label: Featured highlight
|
||||
@@ -123,3 +166,12 @@ form:
|
||||
0: 'No'
|
||||
validate:
|
||||
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
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
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:
|
||||
fields:
|
||||
tabs:
|
||||
@@ -8,6 +16,7 @@ form:
|
||||
fields:
|
||||
|
||||
content:
|
||||
ordering@: 0
|
||||
type: tab
|
||||
title: Content
|
||||
fields:
|
||||
@@ -21,7 +30,15 @@ form:
|
||||
type: markdown
|
||||
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:
|
||||
ordering@: 1
|
||||
type: tab
|
||||
title: Trip
|
||||
fields:
|
||||
@@ -78,17 +95,6 @@ form:
|
||||
'manual': 'Manual — force connect only'
|
||||
'intelligent_gpx': 'Intelligent GPX — suppress where route is covered'
|
||||
|
||||
publishing:
|
||||
type: tab
|
||||
title: Publishing
|
||||
fields:
|
||||
header.published:
|
||||
type: toggle
|
||||
label: Published
|
||||
highlight: 1
|
||||
default: 1
|
||||
options:
|
||||
1: 'Yes'
|
||||
0: 'No'
|
||||
validate:
|
||||
type: bool
|
||||
# No Publishing tab — it held only the Published toggle, which now comes
|
||||
# from the inherited Options tab. Defining it here too would render two
|
||||
# toggles bound to the same header key.
|
||||
|
||||
@@ -2086,7 +2086,7 @@ body::after {
|
||||
.scrolly-step:last-child { padding-bottom: 50vh; }
|
||||
@media (max-width: 768px), (pointer: coarse) {
|
||||
.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:last-child { padding-bottom: 50vh; }
|
||||
}
|
||||
|
||||
@@ -132,23 +132,11 @@
|
||||
update();
|
||||
})();
|
||||
|
||||
/* ── Back to top button ─────────────────────────────────── */
|
||||
(function () {
|
||||
var btn = document.getElementById('story-totop');
|
||||
if (!btn) return;
|
||||
var threshold = window.innerHeight * 0.8;
|
||||
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 });
|
||||
})();
|
||||
/* ── Back to top button ───────────────────────────────────
|
||||
Owned by initBackToTop('story-totop') in js/src/main.js, called from its
|
||||
DOMContentLoaded boot. An inline copy used to live here and double-bound
|
||||
both the click and scroll handlers; it also lacked the history.pushState
|
||||
that makes Back return to the reading position. Do not re-add it. */
|
||||
|
||||
/* ── ChapterBreak scroll-reveal ─────────────────────────── */
|
||||
(function () {
|
||||
@@ -216,11 +204,17 @@
|
||||
update();
|
||||
})();
|
||||
|
||||
/* ── ScrollySection (Scrollama) ──────────────────────────── */
|
||||
(function () {
|
||||
/* ── ScrollySection (Scrollama) ────────────────────────────
|
||||
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 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%'];
|
||||
|
||||
@@ -252,7 +246,10 @@
|
||||
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'); });
|
||||
return;
|
||||
}
|
||||
@@ -281,6 +278,6 @@
|
||||
if (d.direction === 'up') d.element.classList.remove('is-active');
|
||||
});
|
||||
});
|
||||
})();
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||