<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Wanloria</title>
    <description>The latest articles on DEV Community by Wanloria (@wanloria).</description>
    <link>https://dev.to/wanloria</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4160431%2Fd27f51dd-5c29-4a3b-bf55-2a3f2918a6f7.png</url>
      <title>DEV Community: Wanloria</title>
      <link>https://dev.to/wanloria</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/wanloria"/>
    <language>en</language>
    <item>
      <title>How we built a travel guide from open data with Claude Code</title>
      <dc:creator>Wanloria</dc:creator>
      <pubDate>Sat, 03 Oct 2026 19:55:15 +0000</pubDate>
      <link>https://dev.to/wanloria/how-we-built-a-travel-guide-from-open-data-with-claude-code-2eaa</link>
      <guid>https://dev.to/wanloria/how-we-built-a-travel-guide-from-open-data-with-claude-code-2eaa</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Disclosure:&lt;/strong&gt; this post is by the Wanloria team (free, no ads, no affiliate links). It was drafted with AI assistance and fact-checked against the live site, per dev.to's guidelines on AI-assisted posts.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; &lt;a href="https://wanloria.cloud/?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=aeo-devto" rel="noopener noreferrer"&gt;Wanloria&lt;/a&gt; is a free, bilingual (ES/EN) travel web app: verified places in 71 cities, 1-3 day itineraries with times, "free things" lists, signposted hiking trails with GPX and a weekly "this weekend" page. We built it largely by pair-programming with Claude Code and Cursor. The interesting part isn't the AI, though. It's the rules we gave it so it couldn't make things up.&lt;/p&gt;

&lt;h4&gt;
  
  
  The problem: travel content is mostly unverifiable
&lt;/h4&gt;

&lt;p&gt;Most "things to do in X" pages are rewritten from other "things to do in X" pages. We wanted every fact on a page (opening hours, price, website, whether a trail is circular) to trace back to a source, or not be shown at all.&lt;/p&gt;

&lt;h4&gt;
  
  
  Data sources
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;OpenStreetMap&lt;/strong&gt; for places and trail geometry (ODbL, credited on every trail page).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Wikidata / Wikipedia&lt;/strong&gt; for identity and context. Place URLs literally carry the ID: &lt;code&gt;/lugar/wiki-Q1140249/templo-de-debod&lt;/code&gt; and &lt;code&gt;/lugar/osm-r4544509/museo-cerralbo&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Official websites&lt;/strong&gt; for opening hours and prices, stored &lt;em&gt;together with the URL they came from&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Open-Meteo elevation&lt;/strong&gt; (90 m DEM) for elevation profiles.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Rule 1: the LLM writes prose, not facts
&lt;/h4&gt;

&lt;p&gt;Descriptions are generated by an LLM &lt;strong&gt;only from the verified fields&lt;/strong&gt; of that place, never copying sentences from the source. If a fact isn't verified, the template doesn't render it. Every description is reviewed before it's published.&lt;/p&gt;

&lt;h4&gt;
  
  
  Rule 2: computed numbers use published formulas
&lt;/h4&gt;

&lt;p&gt;Hiking time follows &lt;strong&gt;DIN 33466&lt;/strong&gt;, with no stops:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// DIN 33466: 4 km/h on the flat, 300 m/h up, 500 m/h down.&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;dinHours&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;km&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ascentM&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;descentM&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;horizontal&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;km&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;vertical&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;ascentM&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;300&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;descentM&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;horizontal&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;vertical&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;horizontal&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;vertical&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nf"&gt;dinHours&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;12.6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;493&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;395&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// 4.37 h â†’ shown as "4 h 20 min"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's exactly what the live page for the &lt;a href="https://wanloria.cloud/ruta/trail-1948977-41c0d0/collserola-des-de-sant-just?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=aeo-devto" rel="noopener noreferrer"&gt;PR-C 164 near Barcelona&lt;/a&gt; shows (12.6 km, +493 m, 4 h 20 min, MIDE 3/5). Difficulty uses the &lt;strong&gt;MIDE&lt;/strong&gt; 1-5 effort scale. Trails are only published if they're signposted (GR/PR/SL or equivalent), within 18 km of the city, doable in a day, and if the OSM geometry matches the official distance. Loose stages of long-distance routes are dropped.&lt;/p&gt;

&lt;p&gt;Itineraries split a city's top places by area, order each day from most important to nearest, and estimate walking as &lt;strong&gt;straight-line distance + 30 % for streets at 4.5 km/h&lt;/strong&gt;, with a lunch break. Visit duration is estimated by place type (a big museum isn't a viewpoint) and adjusted when the place publishes its own figure.&lt;/p&gt;

&lt;h4&gt;
  
  
  Rule 3: pages for humans and for crawlers
&lt;/h4&gt;

&lt;p&gt;The app itself is a SPA, but every public page (city, place, trail, 1/2/3-day plan, collections like &lt;code&gt;/gratis&lt;/code&gt; and &lt;code&gt;/museos&lt;/code&gt;, and &lt;code&gt;/este-finde&lt;/code&gt;) is &lt;strong&gt;server-rendered HTML&lt;/strong&gt; with JSON-LD (&lt;code&gt;TouristAttraction&lt;/code&gt;, &lt;code&gt;ItemList&lt;/code&gt;, &lt;code&gt;FAQPage&lt;/code&gt;, &lt;code&gt;BreadcrumbList&lt;/code&gt;), hreflang es/en and a sitemap of ~7,000 URLs. There's also an &lt;a href="https://wanloria.cloud/llms.txt" rel="noopener noreferrer"&gt;&lt;code&gt;llms.txt&lt;/code&gt;&lt;/a&gt; and a &lt;code&gt;llms-full.txt&lt;/code&gt; with the core facts per city, each linking to its page.&lt;/p&gt;

&lt;h4&gt;
  
  
  What still goes wrong (honestly)
&lt;/h4&gt;

&lt;p&gt;Verification is only as good as the field it verifies. One real example: a museum whose general ticket costs â‚¬3 but is free on Sundays got tagged "free", because the source said "entrada gratuitaâ€¦" for certain time slots. The fix we're moving to is a richer price model (&lt;code&gt;free&lt;/code&gt;, &lt;code&gt;free_at_times&lt;/code&gt;, &lt;code&gt;paid&lt;/code&gt;) rather than a boolean. Elevation from a 90 m model can be Â±10 % off a GPS track. Opening hours drift, so every place page keeps the source link and users can report errors to &lt;a href="mailto:hola@wanloria.cloud"&gt;hola@wanloria.cloud&lt;/a&gt;.&lt;/p&gt;

&lt;h4&gt;
  
  
  Try it / steal the ideas
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;Methodology: &lt;a href="https://wanloria.cloud/metodologia?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=aeo-devto" rel="noopener noreferrer"&gt;https://wanloria.cloud/metodologia?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=aeo-devto&lt;/a&gt; (EN: /en/methodology)&lt;/li&gt;
&lt;li&gt;Example city: &lt;a href="https://wanloria.cloud/en/destinos/madrid-espana?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=aeo-devto" rel="noopener noreferrer"&gt;Things to do in Madrid&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Example trails: &lt;a href="https://wanloria.cloud/en/rutas/barcelona-espana?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=aeo-devto" rel="noopener noreferrer"&gt;Hiking trails near Barcelona&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Feedback very welcome, especially on the price model and the trail filters.&lt;/p&gt;

</description>
      <category>showdev</category>
      <category>webdev</category>
      <category>opendata</category>
      <category>ai</category>
    </item>
  </channel>
</rss>
