adding prerender, updating blogs, and make images more web friendly

This commit is contained in:
itsamejms
2026-08-24 13:06:15 +01:00
parent 558888118f
commit 8be114f3d7
79 changed files with 2878 additions and 1333 deletions
+31 -31
View File
@@ -1,31 +1,31 @@
robots.txt,1784118671967,a9f80ec8139f358ccf245372fb028866625a4b0d19f44b3aadaeaa0ed1ff8e75
tailtrails-logo.png,1784118671969,dc932ef86246fbf78221ba9128af50a6845890fcbf82101e83c67171d8c520c9
placeholder.svg,1784118671967,e6d62457feaa2cf22a140b4ffae6cc1ec264e50991e369d61f901a3481f9229d
index.html,1784118672031,3f11e47bff0a3720e2fb7f2221aa6d43e049fdd4d4173f8682f6b892be9e1df3
app_store_button.svg,1784118671868,e163505e4516a4dad35281dc952f525f78907a21c49f95dba518a9d206ed45bf
CNAME,1784118672117,dddc5976b778c369189842b7a4c661fb3088f02ceea7dc8d26565f179cd44f95
raw-blogs/walking-your-dog-in-the-algarve-hidden-dangers-every-owner-should-know.md,1784118671967,fba3a0a9b8e8aee35b3300f86363b84e37ac5ab64ab4c3788a3dede919f781e6
raw-blogs/the-importance-of-variety-in-your-dogs-life-and-how-to-do-it-right.md,1784118671967,b9120edf84aab42cea63f9cec38c9c86652b3d9fd8b869fd4c84e871e74050d3
raw-blogs/top-10-hikes-to-do-with-your-dog-in-lagos-and-the-algarve.md,1784118671967,92dbf46e2d921d9366bb4ccfa4508a03867cfc6abd4c76d7c9ea576c86c158c7
raw-blogs/how-to-give-a-reactive-dog-a-fulfilling-life-without-avoiding-the-world.md,1784118671967,cfaf693a02ce30c4453339a13bb7b45333803864c3ce3d4f42c75ec4a00572f5
raw-blogs/off-leash-dog-rules-in-portugal-what-you-need-to-know-in-2026.md,1784118671967,7fbfcbe9b9b6f21837b322073f049857ca09935e07e568d42425c9ff3b15dfaf
raw-blogs/how-to-choose-the-right-walk-for-your-dog-based-on-their-personality-and-needs.md,1784118671967,5c046464b0d04a334cd3c64b7571b2061d033ad3f3894dd0ff4065383e6ac41a
raw-blogs/best-off-leash-areas-for-dogs-in-lagos-and-nearby.md,1784118671967,ba2e88fa252fc3bb096466b16e0abf91578415abdd4662b81efb324f257affe8
404.html,1784118672123,3f11e47bff0a3720e2fb7f2221aa6d43e049fdd4d4173f8682f6b892be9e1df3
assets/index-C8wIY7Lv.css,1784118672030,b5fc631c83de590f9ec0d6959f97ef138ef3556849cbada3dc9c0cb02d5f1619
landing/sad-dog.avif,1784118671966,0d8926a6e4d1ec1f42318c6ee351f17766d57ba0c222c155f0f499753c8a779e
landing/happy-dog.avif,1784118671966,71de609c9e5f4df9bab430649876c17de18696e808935691813da5e12cee8d6b
IMG_2725.PNG,1784118671862,27b6d2b3d3f0639b27edde4a6e4dafd7ff30361b12ed532eea83ea9ff52cc57b
assets/index-CCYERNNs.js,1784118672030,2b953d0955c6573dbd8656b0f8e6fa5b68bfe7d8c448e0045b25eb6bdf0632e1
IMG_2722.PNG,1784118671856,5eb964eca487921be0a641ab01ef44a3a640bc33ee86ef8c78c47beb40c52212
blog-images/troy-bridges-lbRNtUZPXXQ-unsplash.jpg,1784118671917,7cccb32745ffa54a51afdaf9b1ada36ecec8270ff64d57144f2e7936d3800b47
IMG_2724.PNG,1784118671862,1a12277e84b449259e0553359d83de2506f3b85df4198c29d060549bbb5da1ba
blog-images/rafaella-waasdorp-EkzMdwI_YE4-unsplash.jpg,1784118671904,4b06dd88c4b9ee1bb93193f4efefaf5222b45156991d65237580552dbd53a498
blog-images/eric-ward-ISg37AI2A-s-unsplash.jpg,1784118671883,3fe77db2192fcbb773ae6700e784f576d2880d23f3de87675cca7df69e1d62de
blog-images/jamie-street-m4BkRRprvP0-unsplash.jpg,1784118671894,102144b1e3ed572e2e614a7a30bad26d015306005d324e74a67f9391ac7b649d
blog-images/joe-caione-qO-PIF84Vxg-unsplash.jpg,1784118671898,a818800a0777643571a2ac506ab2721e55314c5cc0805d2792fdca20c1f567ef
blog-images/dagmar-klauzova-afBc-k2luH4-unsplash.jpg,1784118671881,a7ef47c2af03e7671ec94dded168d201559b5149c2cafe77b324cbc42f310ad5
blog-images/afra-ramio-cp2HCVzguw4-unsplash.jpg,1784118671874,f5ba745a987168b658a9091041de9bc4ebdbc2e43a6f1f2574c543dc2ed4113c
blog-images/tatum-bergen-Vt3odrTM4cQ-unsplash.jpg,1784118671911,805d069b8cdaa4a18a655f10f6059cc54609c5f568d7c7ea374f19840776d044
IMG_2721.PNG,1784118671856,80bf0292743b93e52e260ea32a347ba5189b6ebcdcf26307c9ec098546dd1b61
hero.mp4,1784118671955,68cf98422d073512761ac7c4a0aa9b11e88b71381c1874422048a578c344af92
index.html,1787241043042,23ab785dcf609ba3797371f540ebbf0da174d23662bda7a91e7d7e5a5129b9f7
robots.txt,1787241042983,a9f80ec8139f358ccf245372fb028866625a4b0d19f44b3aadaeaa0ed1ff8e75
tailtrails-logo.png,1787241042983,dc932ef86246fbf78221ba9128af50a6845890fcbf82101e83c67171d8c520c9
app_store_button.svg,1787241042903,e163505e4516a4dad35281dc952f525f78907a21c49f95dba518a9d206ed45bf
placeholder.svg,1787241042982,e6d62457feaa2cf22a140b4ffae6cc1ec264e50991e369d61f901a3481f9229d
404.html,1787241043108,23ab785dcf609ba3797371f540ebbf0da174d23662bda7a91e7d7e5a5129b9f7
CNAME,1787241043103,dddc5976b778c369189842b7a4c661fb3088f02ceea7dc8d26565f179cd44f95
raw-blogs/walking-your-dog-in-the-algarve-hidden-dangers-every-owner-should-know.md,1787241042983,fba3a0a9b8e8aee35b3300f86363b84e37ac5ab64ab4c3788a3dede919f781e6
raw-blogs/top-10-hikes-to-do-with-your-dog-in-lagos-and-the-algarve.md,1787241042982,92dbf46e2d921d9366bb4ccfa4508a03867cfc6abd4c76d7c9ea576c86c158c7
raw-blogs/best-off-leash-areas-for-dogs-in-lagos-and-nearby.md,1787241042982,ba2e88fa252fc3bb096466b16e0abf91578415abdd4662b81efb324f257affe8
raw-blogs/off-leash-dog-rules-in-portugal-what-you-need-to-know-in-2026.md,1787241042982,7fbfcbe9b9b6f21837b322073f049857ca09935e07e568d42425c9ff3b15dfaf
raw-blogs/the-importance-of-variety-in-your-dogs-life-and-how-to-do-it-right.md,1787241042982,b9120edf84aab42cea63f9cec38c9c86652b3d9fd8b869fd4c84e871e74050d3
raw-blogs/how-to-choose-the-right-walk-for-your-dog-based-on-their-personality-and-needs.md,1787241042982,5c046464b0d04a334cd3c64b7571b2061d033ad3f3894dd0ff4065383e6ac41a
raw-blogs/how-to-give-a-reactive-dog-a-fulfilling-life-without-avoiding-the-world.md,1787241042982,cfaf693a02ce30c4453339a13bb7b45333803864c3ce3d4f42c75ec4a00572f5
assets/index-CU78BVRf.css,1787241043042,7c88342b634dcb962b09e80964ec25efb0417b3b84a9ea4d94a370b59e5ad4d0
landing/sad-dog.avif,1787241042982,0d8926a6e4d1ec1f42318c6ee351f17766d57ba0c222c155f0f499753c8a779e
landing/happy-dog.avif,1787241042978,71de609c9e5f4df9bab430649876c17de18696e808935691813da5e12cee8d6b
IMG_2725.PNG,1787241042899,27b6d2b3d3f0639b27edde4a6e4dafd7ff30361b12ed532eea83ea9ff52cc57b
assets/index-DmqQSK7M.js,1787241043042,c4e28b173525e94180d05b2f9b60f031ecb761ab9597cfa49a92763375f6f1bc
IMG_2722.PNG,1787241042891,5eb964eca487921be0a641ab01ef44a3a640bc33ee86ef8c78c47beb40c52212
blog-images/troy-bridges-lbRNtUZPXXQ-unsplash.jpg,1787241042945,7cccb32745ffa54a51afdaf9b1ada36ecec8270ff64d57144f2e7936d3800b47
IMG_2724.PNG,1787241042899,1a12277e84b449259e0553359d83de2506f3b85df4198c29d060549bbb5da1ba
blog-images/rafaella-waasdorp-EkzMdwI_YE4-unsplash.jpg,1787241042935,4b06dd88c4b9ee1bb93193f4efefaf5222b45156991d65237580552dbd53a498
blog-images/eric-ward-ISg37AI2A-s-unsplash.jpg,1787241042920,3fe77db2192fcbb773ae6700e784f576d2880d23f3de87675cca7df69e1d62de
blog-images/jamie-street-m4BkRRprvP0-unsplash.jpg,1787241042925,102144b1e3ed572e2e614a7a30bad26d015306005d324e74a67f9391ac7b649d
blog-images/joe-caione-qO-PIF84Vxg-unsplash.jpg,1787241042930,a818800a0777643571a2ac506ab2721e55314c5cc0805d2792fdca20c1f567ef
blog-images/dagmar-klauzova-afBc-k2luH4-unsplash.jpg,1787241042914,a7ef47c2af03e7671ec94dded168d201559b5149c2cafe77b324cbc42f310ad5
blog-images/afra-ramio-cp2HCVzguw4-unsplash.jpg,1787241042907,f5ba745a987168b658a9091041de9bc4ebdbc2e43a6f1f2574c543dc2ed4113c
blog-images/tatum-bergen-Vt3odrTM4cQ-unsplash.jpg,1787241042938,805d069b8cdaa4a18a655f10f6059cc54609c5f568d7c7ea374f19840776d044
IMG_2721.PNG,1787241042890,80bf0292743b93e52e260ea32a347ba5189b6ebcdcf26307c9ec098546dd1b61
hero.mp4,1787241042966,68cf98422d073512761ac7c4a0aa9b11e88b71381c1874422048a578c344af92
+366 -32
View File
@@ -14,7 +14,6 @@
"class-variance-authority": "0.7.1",
"clsx": "2.1.1",
"lucide-react": "^0.577.0",
"marked": "^18.0.2",
"motion": "^12.23.24",
"react": "^19.2.4",
"react-dom": "^19.2.4",
@@ -27,6 +26,7 @@
"@types/react-dom": "^19.2.3",
"autoprefixer": "^10.4.27",
"gh-pages": "^6.3.0",
"puppeteer": "^25.8.0",
"tailwindcss": "^4.2.2",
"tsx": "^4.21.0",
"typescript": "~5.8.2",
@@ -588,6 +588,35 @@
"url": "https://github.com/sponsors/Boshen"
}
},
"node_modules/@puppeteer/browsers": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-3.2.1.tgz",
"integrity": "sha512-KDz+3qDRdBAlRlMjmKyj6dEs33YHTk/xRHEENSXq6TNnhgoU15ruSHtEBeVF6OZ9tBDY55Se4P0nFMNsipzU9A==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"modern-tar": "^0.8.0",
"yargs": "^18.0.0"
},
"bin": {
"browsers": "lib/main-cli.js"
},
"engines": {
"node": ">=22.12.0"
},
"peerDependencies": {
"proxy-agent": ">=8.0.1",
"yauzl": "^2.10.0 || ^3.4.0"
},
"peerDependenciesMeta": {
"proxy-agent": {
"optional": true
},
"yauzl": {
"optional": true
}
}
},
"node_modules/@radix-ui/primitive": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz",
@@ -1421,17 +1450,6 @@
"tslib": "^2.4.0"
}
},
"node_modules/@types/node": {
"version": "22.19.15",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.15.tgz",
"integrity": "sha512-F0R/h2+dsy5wJAUe3tAU6oqa2qbWY5TpNfL/RGmo1y38hiyO1w3x2jPtt76wmuaJI4DQnOBu21cNXQ2STIUUWg==",
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"undici-types": "~6.21.0"
}
},
"node_modules/@types/react": {
"version": "19.2.14",
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz",
@@ -1477,6 +1495,32 @@
}
}
},
"node_modules/ansi-regex": {
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.3.0.tgz",
"integrity": "sha512-WpDfL7NO6j7tH88IDBNVdUJxDh9nmCteAVW9dsep846XdwF4naCBK+/tGLX3KJgcpgMRXCFlTM2hKGoK9FsdrQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/ansi-regex?sponsor=1"
}
},
"node_modules/ansi-styles": {
"version": "6.2.3",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz",
"integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/array-union": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz",
@@ -1612,6 +1656,23 @@
],
"license": "CC-BY-4.0"
},
"node_modules/chromium-bidi": {
"version": "17.0.2",
"resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-17.0.2.tgz",
"integrity": "sha512-5v9GQFhTktFvotn/OFNJBmKLKRAb6n9r0bVCwf7sHgWc3/JryK0bj1nn93L3pHFrfgcsu6Be6EWsDi+1XHTGDg==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"mitt": "^3.0.1",
"zod": "^3.24.1"
},
"engines": {
"node": ">=20.19.0 <22.0.0 || >=22.12.0"
},
"peerDependencies": {
"devtools-protocol": "*"
}
},
"node_modules/class-variance-authority": {
"version": "0.7.1",
"resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz",
@@ -1624,6 +1685,39 @@
"url": "https://polar.sh/cva"
}
},
"node_modules/cliui": {
"version": "9.0.1",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-9.0.1.tgz",
"integrity": "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==",
"dev": true,
"license": "ISC",
"dependencies": {
"string-width": "^7.2.0",
"strip-ansi": "^7.1.0",
"wrap-ansi": "^9.0.0"
},
"engines": {
"node": ">=20"
}
},
"node_modules/cliui/node_modules/string-width": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz",
"integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"emoji-regex": "^10.3.0",
"get-east-asian-width": "^1.0.0",
"strip-ansi": "^7.1.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/clsx": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
@@ -1679,6 +1773,13 @@
"node": ">=8"
}
},
"node_modules/devtools-protocol": {
"version": "0.0.1666840",
"resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1666840.tgz",
"integrity": "sha512-gCcO42XCHKEs7Ag0S7aGYsnJ7hlgrO3qderYqeiY0Eqk+0GFfuvT13IA0hHreJTa2KCdDVyGMeOhdMNmrrTjVg==",
"dev": true,
"license": "BSD-3-Clause"
},
"node_modules/dir-glob": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz",
@@ -1706,6 +1807,13 @@
"dev": true,
"license": "MIT"
},
"node_modules/emoji-regex": {
"version": "10.6.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz",
"integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==",
"dev": true,
"license": "MIT"
},
"node_modules/enhanced-resolve": {
"version": "5.20.1",
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.20.1.tgz",
@@ -1951,6 +2059,29 @@
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/get-caller-file": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
"integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
"dev": true,
"license": "ISC",
"engines": {
"node": "6.* || 8.* || >= 10.*"
}
},
"node_modules/get-east-asian-width": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.6.0.tgz",
"integrity": "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/get-tsconfig": {
"version": "4.13.6",
"resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.6.tgz",
@@ -2341,6 +2472,19 @@
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lilconfig": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz",
"integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/sponsors/antonk52"
}
},
"node_modules/locate-path": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
@@ -2388,18 +2532,6 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/marked": {
"version": "18.0.2",
"resolved": "https://registry.npmjs.org/marked/-/marked-18.0.2.tgz",
"integrity": "sha512-NsmlUYBS/Zg57rgDWMYdnre6OTj4e+qq/JS2ot3KrYLSoHLw+sDu0Nm1ZGpRgYAq6c+b1ekaY5NzVchMCQnzcg==",
"license": "MIT",
"bin": {
"marked": "bin/marked.js"
},
"engines": {
"node": ">= 20"
}
},
"node_modules/merge2": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
@@ -2424,6 +2556,23 @@
"node": ">=8.6"
}
},
"node_modules/mitt": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz",
"integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==",
"dev": true,
"license": "MIT"
},
"node_modules/modern-tar": {
"version": "0.8.4",
"resolved": "https://registry.npmjs.org/modern-tar/-/modern-tar-0.8.4.tgz",
"integrity": "sha512-gN54ddmyzEg10orwZ2u4OOv+bjpMWdIl5jIkodK97bMq8QBSL5c0D7YX0lT1Ooz+99S7+PvFbnxzdjgHo1r41g==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=18.0.0"
}
},
"node_modules/motion": {
"version": "12.38.0",
"resolved": "https://registry.npmjs.org/motion/-/motion-12.38.0.tgz",
@@ -2616,6 +2765,46 @@
"dev": true,
"license": "MIT"
},
"node_modules/puppeteer": {
"version": "25.8.0",
"resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-25.8.0.tgz",
"integrity": "sha512-3gcUJ+Jfodb5zNa/lWLZukBUwYiRIwAc8WRICoqfi+ZYmNWqpsPFyanTU3Gw/lhgII9aotVbAmhT6/NKHWgyUA==",
"dev": true,
"hasInstallScript": true,
"license": "Apache-2.0",
"dependencies": {
"@puppeteer/browsers": "3.2.1",
"chromium-bidi": "17.0.2",
"devtools-protocol": "0.0.1666840",
"lilconfig": "^3.1.3",
"puppeteer-core": "25.8.0",
"typed-query-selector": "^2.12.2"
},
"bin": {
"puppeteer": "lib/puppeteer/node/cli.js"
},
"engines": {
"node": ">=22.12.0"
}
},
"node_modules/puppeteer-core": {
"version": "25.8.0",
"resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-25.8.0.tgz",
"integrity": "sha512-LDOrawV8vfCVk+yLj2ozvajNP4Sv3OV9y3Tpiyy2g2Z+aQlbcozP6KJfI4iSBq7YQER+86ihEtPa5ioiZyWxMQ==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@puppeteer/browsers": "3.2.1",
"chromium-bidi": "17.0.2",
"devtools-protocol": "0.0.1666840",
"typed-query-selector": "^2.12.2",
"webdriver-bidi-protocol": "0.4.2",
"ws": "^8.21.1"
},
"engines": {
"node": ">=22.12.0"
}
},
"node_modules/queue-microtask": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
@@ -2840,6 +3029,39 @@
"node": ">=0.10.0"
}
},
"node_modules/string-width": {
"version": "8.2.2",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-8.2.2.tgz",
"integrity": "sha512-GaPUh5gfdrYzqeVNZvUfT23vYYxXzKYidUcnMtJg/3rxRV63EFZy3k6xfKlmfeJD0176lnUV/Usr3XcwSvFzpg==",
"dev": true,
"license": "MIT",
"dependencies": {
"get-east-asian-width": "^1.5.0",
"strip-ansi": "^7.1.2"
},
"engines": {
"node": ">=20"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/strip-ansi": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz",
"integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-regex": "^6.2.2"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/strip-ansi?sponsor=1"
}
},
"node_modules/strip-outer": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/strip-outer/-/strip-outer-1.0.1.tgz",
@@ -2979,6 +3201,13 @@
"fsevents": "~2.3.3"
}
},
"node_modules/typed-query-selector": {
"version": "2.12.2",
"resolved": "https://registry.npmjs.org/typed-query-selector/-/typed-query-selector-2.12.2.tgz",
"integrity": "sha512-EOPFbyIub4ngnEdqi2yOcNeDLaX/0jcE1JoAXQDDMIthap7FoN795lc/SHfIq2d416VufXpM8z/lD+WRm2gfOQ==",
"dev": true,
"license": "MIT"
},
"node_modules/typescript": {
"version": "5.8.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
@@ -2993,14 +3222,6 @@
"node": ">=14.17"
}
},
"node_modules/undici-types": {
"version": "6.21.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
"license": "MIT",
"optional": true,
"peer": true
},
"node_modules/universalify": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
@@ -3130,6 +3351,119 @@
"funding": {
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/webdriver-bidi-protocol": {
"version": "0.4.2",
"resolved": "https://registry.npmjs.org/webdriver-bidi-protocol/-/webdriver-bidi-protocol-0.4.2.tgz",
"integrity": "sha512-VSV+fzfChirL3e7jay2yUC7B4HQCGtEWEg/MSSQbK+qWbqeGlRLlXTzPpYr3XGUvbpDHumWZBJxgesg4N7dbtA==",
"dev": true,
"license": "Apache-2.0"
},
"node_modules/wrap-ansi": {
"version": "9.0.2",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz",
"integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-styles": "^6.2.1",
"string-width": "^7.0.0",
"strip-ansi": "^7.1.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
}
},
"node_modules/wrap-ansi/node_modules/string-width": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz",
"integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"emoji-regex": "^10.3.0",
"get-east-asian-width": "^1.0.0",
"strip-ansi": "^7.1.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/ws": {
"version": "8.21.3",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.21.3.tgz",
"integrity": "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=10.0.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": ">=5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {
"optional": true
},
"utf-8-validate": {
"optional": true
}
}
},
"node_modules/y18n": {
"version": "5.0.8",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
"integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
"dev": true,
"license": "ISC",
"engines": {
"node": ">=10"
}
},
"node_modules/yargs": {
"version": "18.1.0",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-18.1.0.tgz",
"integrity": "sha512-2rAgRKu54VsHkqI0/tYkmluGXHD4KW7yZoycuqDQ15QOTnc2VVfy0nN/1eMhnQLO00A+dwtK20xuCnc1YGeUyg==",
"dev": true,
"license": "MIT",
"dependencies": {
"cliui": "^9.0.1",
"escalade": "^3.1.1",
"get-caller-file": "^2.0.5",
"string-width": "^8.2.1",
"y18n": "^5.0.5",
"yargs-parser": "^22.0.0"
},
"engines": {
"node": "^20.19.0 || ^22.12.0 || >=23"
}
},
"node_modules/yargs-parser": {
"version": "22.0.0",
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-22.0.0.tgz",
"integrity": "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==",
"dev": true,
"license": "ISC",
"engines": {
"node": "^20.19.0 || ^22.12.0 || >=23"
}
},
"node_modules/zod": {
"version": "3.25.76",
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
"dev": true,
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
}
}
}
+2 -2
View File
@@ -5,7 +5,7 @@
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"build": "node scripts/generate-sitemap.mjs && vite build && node scripts/prerender.mjs && node scripts/postbuild.cjs",
"postbuild": "node ./scripts/postbuild.cjs",
"predeploy": "npm run build",
"deploy": "gh-pages -d dist",
@@ -20,7 +20,6 @@
"class-variance-authority": "0.7.1",
"clsx": "2.1.1",
"lucide-react": "^0.577.0",
"marked": "^18.0.2",
"motion": "^12.23.24",
"react": "^19.2.4",
"react-dom": "^19.2.4",
@@ -33,6 +32,7 @@
"@types/react-dom": "^19.2.3",
"autoprefixer": "^10.4.27",
"gh-pages": "^6.3.0",
"puppeteer": "^25.8.0",
"tailwindcss": "^4.2.2",
"tsx": "^4.21.0",
"typescript": "~5.8.2",
Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 926 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 152 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 115 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 254 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 122 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 172 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 176 KiB

BIN
View File
Binary file not shown.
@@ -1,126 +0,0 @@
### **Introduction**
Finding a good place to let your dog off-leash in Lagos isn’t always straightforward.
Some areas are:
* Too busy
* Too unpredictable
* Or not actually suitable for your dog
But when you find the right spot, it changes everything.
Here are some of the best off-leash-friendly areas around Lagos, and how to choose the right one for your dog.
---
## **1. Mata Nacional de Barão de São João**
**Best for:** Most dogs
**Why it works:**
* Wide, open forest trails
* Plenty of space to create distance
* Shaded and cooler
Ideal for dogs who need **space without isolation**
---
## **2. Praia da Bordeira (West Coast)**
**Best for:** High-energy dogs
**Why it works:**
* Huge open beach
* Low crowd density (outside peak times)
* Great for running
Watch:
* Wind exposure
* Seasonal rules
---
## **3. Ria de Alvor (Quiet Sections)**
**Best for:** Lower-energy or calm dogs
**Why it works:**
* Flat terrain
* Open visibility
Avoid peak hours, it can get busy
---
## **4. Monchique Countryside**
**Best for:** Dogs needing low stimulation
**Why it works:**
* Quiet
* Natural
* Cooler temperatures
Great for reactive or sensitive dogs
---
## **5. Local Countryside Trails (Hidden Gems)**
Often overlooked, but some of the best options.
These areas offer:
* Lower dog traffic
* More control
* Flexible space
Especially useful for:
* Training recall
* Building confidence
Check out trails near countryside villages like Bensafrim and Vila do Bispo.
---
## **Important: Not Every “Off-Leash Area” Is Right**
Just because a place allows off-leash doesn’t mean it’s suitable.
Before letting your dog off:
* Check the environment
* Look at dog traffic
* Consider your dog’s behaviour
---
## **What Makes a Good Off-Leash Spot?**
The best places have:
* Space to move freely
* Clear visibility
* Low unpredictability
* An environment suited to your dog
---
## **Final Thoughts**
The goal isn’t just to let your dog off-leash.
It’s to do it in a way that:
* Feels safe
* Feels calm
* And actually benefits your dog
Because the right environment makes all the difference.
<!-- **Want help finding the right trails for your dog? Download TailTrails here.** -->
@@ -1,131 +0,0 @@
### **Introduction**
Choosing a walk for your dog often comes down to convenience.
What’s nearby.
What you’ve done before.
What seems “good enough.”
But the best walks aren’t chosen that way.
They’re chosen based on **your dog**.
---
### **Step 1: Understand Your Dog’s Profile**
Start with the basics:
* Energy level (low, medium, high)
* Sociability (loves dogs vs prefers space)
* Confidence (bold vs cautious)
* Sensitivities (noise, movement, crowds)
* Physical needs (age, injuries, endurance)
This becomes your baseline.
---
### **Step 2: Match the Environment**
Now consider the walk itself:
#### **High-energy dogs**
Need:
* Long routes
* Open space
* Opportunities to run
Best environments:
* Wide trails
* Open countryside
* Low restriction areas
---
**Sensitive or reactive dogs**
Need:
* Predictability
* Space from others
* Low stimulation
Best environments:
* Quiet trails
* Low dog traffic areas
* Wide paths with visibility
---
#### **Older or recovering dogs**
Need:
* Shorter distances
* Even terrain
* Shade
Best environments:
* Flat trails
* Forest paths
* Low-exertion routes
---
### **Step 3: Look Beyond the Surface**
A trail might look perfect, but details matter:
* Is it narrow or open?
* Is there shade?
* Are dogs typically off-leash there?
* Is it busy at certain times?
These small factors make a big difference.
---
### **Step 4: Pay Attention to Feedback**
Your dog will tell you if it’s working.
Look for:
* Relaxed body language
* Natural curiosity
* Steady energy (not frantic or shut down)
* Willing engagement with surroundings
If you’re seeing stress signals, it may not be the right fit.
---
### **Step 5: Prioritise Variety Intentionally**
Variety is important.
But random variety isn’t helpful.
Instead:
* Rotate between environments that suit your dog
* Gradually introduce new challenges
* Build confidence over time
---
### **Final Thoughts**
The best walk isn’t the longest or the most popular.
It’s the one that fits your dog.
Because when you choose the right environment, you’re not just going for a walk, you’re creating a better experience for both of you.
<!-- **Want help finding the right trails for your dog? Download TailTrails here.** -->
@@ -1,120 +0,0 @@
### **Introduction**
Living with a reactive dog can feel limiting.
You might:
* Avoid certain places
* Walk at odd hours
* Stick to the same safe routes
But here’s the truth:
Your dog can still have a full, enriching life.
It just requires a different approach.
---
## **What Reactivity Often Means**
Reactivity isn’t “bad behaviour.”
It’s usually:
* Overstimulation
* Fear or uncertainty
* Lack of space
* Too much, too quickly
---
## **Environment Matters More Than Training Alone**
Training is important, but environment is foundational.
Even a well-trained dog can struggle in:
* Busy areas
* Narrow paths
* High dog-traffic zones
---
## **Choosing the Right Environments**
Look for places that offer:
* Space to move away
* Predictability
* Lower levels of stimulation
Examples:
* Wide trails
* Quiet countryside
* Off-peak walking times
---
## **Give Them the Right Kind of Freedom**
Reactive dogs don’t need:
* More pressure
* More exposure too quickly
They need:
* Controlled experiences
* Space to decompress
* Opportunities to succeed
---
## **Common Mistakes**
* Taking them to busy “dog-friendly” areas
* Forcing social interactions
* Ignoring early stress signals
---
## **Building Confidence Over Time**
Start small.
* Familiar environments
* Gradual changes
* Positive reinforcement
Confidence grows through:
Consistent, successful experiences
---
## **Fulfilment Looks Different**
A fulfilling life doesn’t mean:
* Busy parks
* Social environments
It means:
* Calm exploration
* Comfortable movement
* Feeling safe in the world
---
## **Final Thoughts**
Your dog doesn’t need to handle everything.
They just need the **right environments to thrive in**.
And when you give them that, everything starts to shift.
<!-- **Want help finding the right trails for your dog? Download TailTrails here.** -->
@@ -1,125 +0,0 @@
### **Introduction**
Letting your dog off-leash is one of the simplest ways to give them freedom, enrichment, and a more natural experience.
But if you’re living in or visiting Portugal, the rules aren’t always obvious, and getting it wrong can lead to fines, unsafe situations, or conflict with others.
Here’s what you need to know.
---
### **Are Dogs Allowed Off-Leash in Portugal?**
In general, dogs in Portugal are expected to be **under control at all times in public spaces**.
That doesn’t always mean physically on a leash - but it *does* mean:
* Your dog must respond reliably to commands
* They must not pose a risk to people, other animals, or property
* They must not cause disturbance
In many **urban areas**, leash use is effectively required.
In more **rural or remote areas**, off-leash walking is more commonly accepted - but still comes with responsibility.
---
### **Official Rules, Restrictions, and Fines**
In Portugal, there are also specific legal restrictions to be aware of, especially around beaches and public spaces. During the summer season (typically June to September), many beaches prohibit dogs entirely, regardless of whether they are on or off-leash. These rules are set by local municipalities and are usually clearly signposted at beach entrances. Outside of summer, some beaches become dog-friendly again, but leash expectations can still apply.
Failing to follow these rules can result in on-the-spot fines, which typically range from around €25 up to €250 or more, depending on the situation and location. Additional penalties may apply if a dog is considered out of control, causes disturbance, or poses a risk to others.
Because enforcement and rules can vary by region, it’s always worth checking local signage before letting your dog off-leash, especially in popular or seasonal areas.
---
### **Where Off-Leash Walking Is (Usually) Acceptable**
You’re more likely to safely and responsibly walk your dog off-leash in:
* Quiet countryside trails
* Forest paths with low foot traffic
* Remote coastal routes (away from crowds)
* Open land where visibility is high
However, even in these areas, it’s not just about permission - it’s about suitability for your dog.
---
### **Where You Should Keep Your Dog On-Leash**
There are several environments where off-leash walking is **not recommended or restricted**:
* Busy beaches (especially during peak season)
* Urban parks and promenades
* Narrow cliff paths
* Areas with livestock or wildlife
* High dog-traffic zones
Even if technically allowed, these environments can be:
* Overstimulating
* Unsafe
* Stressful for reactive or sensitive dogs
---
### **The Risks Most Dog Owners Overlook**
Many owners assume off-leash = freedom.
But without the right environment, it can quickly become the opposite.
Common risks include:
* Uncontrolled interactions with other dogs
* Encounters with wildlife or livestock
* Sudden terrain hazards (cliffs, unstable ground)
* Overheating in exposed areas
* Poor recall in unfamiliar settings
The result? Stress for both you *and* your dog.
---
### **Why “Dog-Friendly” Isn’t Enough**
A beach might allow dogs.
A trail might be popular with dog walkers.
But that doesn’t mean it’s **right for your dog**.
Every dog has different needs:
* Some thrive in social, high-energy spaces
* Others need quiet, predictable environments
* Some need open space to run
* Others need structured, low-stimulation walks
Choosing the wrong environment can undo the very thing you’re trying to give them.
---
### **How to Choose the Right Off-Leash Environment**
Before letting your dog off-leash, ask:
* How busy is this area?
* Are there other dogs nearby?
* Is the terrain safe and visible?
* Is there wildlife or livestock?
* Will my dog feel confident here, or overwhelmed?
The goal isn’t just freedom.
It’s **setting your dog up for a good time**.
---
### **Final Thoughts**
Walking your dog off-leash in Portugal can be an awesome experience, when it’s done in the right place, at the right time, for the right dog.
Because it’s not just about whether dogs are allowed.
It’s about whether the environment actually fits.
<!-- **Want help finding the right trails for your dog? Download TailTrails here.** -->
@@ -1,115 +0,0 @@
### **Introduction**
Most dogs live very predictable lives.
The same route.
The same pace.
The same environment.
And while routine has its place, too much of it can limit your dog’s experience of the world.
---
## **Why Variety Matters**
Dogs are naturally curious.
They’re wired to:
* Explore
* Smell
* Navigate new environments
When every walk is the same, they miss out on:
* Mental stimulation
* Physical engagement
* Confidence-building experiences
---
## **It’s Not Just About Exercise**
A longer walk doesn’t always mean a better walk.
A short walk in a new environment can be:
* More stimulating
* More engaging
* More fulfilling
---
## **The Benefits of Variety**
Introducing new environments can:
* Reduce boredom
* Improve behaviour
* Build confidence
* Strengthen your bond
---
## **But Random Variety Isn’t the Answer**
This is where many owners get it wrong.
Throwing your dog into:
* Busy environments
* Unpredictable situations
* Overstimulating spaces
Can actually:
* Increase stress
* Lead to reactivity
* Create negative experiences
---
## **How to Introduce Variety Properly**
### **1. Start with similar environments**
If your dog is used to quiet trails, try new *quiet* trails first.
---
### **2. Change one variable at a time**
* New location, same energy level
* New terrain, same level of stimulation
---
### **3. Watch your dog’s response**
Let their behaviour guide your decisions.
---
### **4. Build gradually**
Confidence grows with positive experiences.
---
## **The Goal**
Not constant novelty.
But **intentional variation** that suits your dog.
---
## **Final Thoughts**
Your dog doesn’t need more walks.
They need **better, more varied experiences**.
Because when you get it right, even a simple walk can feel completely different.
<!-- **Want help finding the right trails for your dog? Download TailTrails here.** -->
@@ -1,86 +0,0 @@
### **Introduction**
If you’re based in or visiting Lagos, you’re surrounded by some of the most beautiful walking routes in Europe.
But not all hikes are equal - especially when you’re walking with a dog.
Some are:
* Too exposed
* Too busy
* Too steep
* Or just not suited to your dog’s needs
Here are 10 of the best dog-friendly hikes in the Algarve, plus what makes each one a good fit.
---
## **1. Ponta da Piedade Coastal Walk**
**Best for:** Calm, confident dogs
**Type:** Cliffside / scenic
Incredible views, but:
* Narrow in parts
* High foot traffic
Best early morning or off-season
---
## **2. Mata Nacional de Barão de São João**
**Best for:** Most dogs
**Type:** Forest / shaded
* Wide trails
* Plenty of shade
* Lower stimulation
---
## **3. Ria de Alvor Nature Reserve**
**Best for:** Low to moderate energy dogs
**Type:** Boardwalk / coastal
* Flat and accessible
* Can be busy
* Limited shade
---
## **How to Choose the Right One**
Instead of asking:
“Which hike is best?”
Ask:
“Which hike is right for my dog?”
Consider:
* Energy level
* Sociability
* Confidence
* Weather
* Time of day
---
## **Final Thoughts**
The Algarve is full of incredible places to explore with your dog.
But the best walks aren’t just beautiful.
They’re the ones that:
* Suit your dog
* Feel comfortable
* And allow them to be at their best
<!-- **Want help finding the right trails for your dog? Download TailTrails here.** -->
@@ -1,71 +0,0 @@
### **Introduction**
The Algarve is one of the best places in Europe to explore with your dog.
Cliffside trails.
Quiet forests.
Open countryside.
But beneath all that beauty, there are a few **less obvious risks** that many dog owners (especially newcomers) don’t see coming.
Knowing what to look out for can make the difference between a great walk and an emergency vet visit.
---
## **1. Pine Processionary Caterpillars (High Risk)**
These are one of the **most dangerous hazards** for dogs in Portugal.
They appear in:
* Pine forests
* Woodland trails
* Warmer months (typically late winter to spring)
### **Why they’re dangerous:**
Their tiny hairs contain a toxin that can cause:
* Severe swelling of the mouth and tongue
* Vomiting
* Difficulty breathing
* Tissue damage (in serious cases)
### **What to look for:**
* Caterpillars moving in a line (procession)
* Nests in pine trees (silk-like bundles)
---
## **2. Grass Seeds (Foxtails)**
Small, sharp, and incredibly problematic.
They’re common in:
* Dry grasslands
* Trail edges
* Late spring and summer
### **Why they’re dangerous:**
They can lodge in:
* Noses
* Ears
* Eyes
* Between toes
---
## **Final Thoughts**
Walking your dog in the Algarve can be one of the best parts of living or visiting here.
But the best walks happen when you:
* Know what to look out for
* Choose environments intentionally
* Match the walk to your dog’s needs
<!-- **Want help finding the right trails for your dog? Download TailTrails here.** -->
+2
View File
@@ -12,3 +12,5 @@ Allow: /
User-agent: *
Allow: /
Sitemap: https://tailtrails.club/sitemap.xml
+123
View File
@@ -0,0 +1,123 @@
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url>
<loc>https://tailtrails.club/</loc>
<lastmod>2026-08-24</lastmod>
<changefreq>weekly</changefreq>
<priority>1.0</priority>
</url>
<url>
<loc>https://tailtrails.club/blog</loc>
<lastmod>2026-08-24</lastmod>
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://tailtrails.club/compare</loc>
<lastmod>2026-08-24</lastmod>
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://tailtrails.club/compare/dog-walking-apps-compared</loc>
<lastmod>2026-08-24</lastmod>
<changefreq>monthly</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>https://tailtrails.club/compare/tailtrails-vs-wooftrax</loc>
<lastmod>2026-08-24</lastmod>
<changefreq>monthly</changefreq>
<priority>0.7</priority>
</url>
<url>
<loc>https://tailtrails.club/compare/tailtrails-vs-tractive</loc>
<lastmod>2026-08-24</lastmod>
<changefreq>monthly</changefreq>
<priority>0.7</priority>
</url>
<url>
<loc>https://tailtrails.club/compare/tailtrails-vs-alltrails</loc>
<lastmod>2026-08-24</lastmod>
<changefreq>monthly</changefreq>
<priority>0.7</priority>
</url>
<url>
<loc>https://tailtrails.club/compare/tailtrails-vs-fitbark</loc>
<lastmod>2026-08-24</lastmod>
<changefreq>monthly</changefreq>
<priority>0.7</priority>
</url>
<url>
<loc>https://tailtrails.club/compare/tailtrails-vs-pitpat</loc>
<lastmod>2026-08-24</lastmod>
<changefreq>monthly</changefreq>
<priority>0.7</priority>
</url>
<url>
<loc>https://tailtrails.club/privacy</loc>
<lastmod>2026-08-24</lastmod>
<changefreq>yearly</changefreq>
<priority>0.3</priority>
</url>
<url>
<loc>https://tailtrails.club/terms</loc>
<lastmod>2026-08-24</lastmod>
<changefreq>yearly</changefreq>
<priority>0.3</priority>
</url>
<url>
<loc>https://tailtrails.club/forget-me</loc>
<lastmod>2026-08-24</lastmod>
<changefreq>yearly</changefreq>
<priority>0.3</priority>
</url>
<url>
<loc>https://tailtrails.club/finalize-forget-me</loc>
<lastmod>2026-08-24</lastmod>
<changefreq>yearly</changefreq>
<priority>0.3</priority>
</url>
<url>
<loc>https://tailtrails.club/blog/best-off-leash-areas-for-dogs-in-lagos-and-nearby</loc>
<lastmod>2026-08-24</lastmod>
<changefreq>monthly</changefreq>
<priority>0.7</priority>
</url>
<url>
<loc>https://tailtrails.club/blog/how-to-choose-the-right-walk-for-your-dog-based-on-their-personality-and-needs</loc>
<lastmod>2026-08-24</lastmod>
<changefreq>monthly</changefreq>
<priority>0.7</priority>
</url>
<url>
<loc>https://tailtrails.club/blog/how-to-give-a-reactive-dog-a-fulfilling-life-without-avoiding-the-world</loc>
<lastmod>2026-08-24</lastmod>
<changefreq>monthly</changefreq>
<priority>0.7</priority>
</url>
<url>
<loc>https://tailtrails.club/blog/off-leash-dog-rules-in-portugal-what-you-need-to-know-in-2026</loc>
<lastmod>2026-08-24</lastmod>
<changefreq>monthly</changefreq>
<priority>0.7</priority>
</url>
<url>
<loc>https://tailtrails.club/blog/the-importance-of-variety-in-your-dogs-life-and-how-to-do-it-right</loc>
<lastmod>2026-08-24</lastmod>
<changefreq>monthly</changefreq>
<priority>0.7</priority>
</url>
<url>
<loc>https://tailtrails.club/blog/top-10-hikes-to-do-with-your-dog-in-lagos-and-the-algarve</loc>
<lastmod>2026-08-24</lastmod>
<changefreq>monthly</changefreq>
<priority>0.7</priority>
</url>
<url>
<loc>https://tailtrails.club/blog/walking-your-dog-in-the-algarve-hidden-dangers-every-owner-should-know</loc>
<lastmod>2026-08-24</lastmod>
<changefreq>monthly</changefreq>
<priority>0.7</priority>
</url>
</urlset>
+46
View File
@@ -0,0 +1,46 @@
#!/usr/bin/env bash
# Generate one topical image per blog post with stable-diffusion.cpp
# using Juggernaut XL v9 (photoreal SDXL fine-tune).
# euler_a + 1024² + no --mmap is the stable combo on Apple Metal here
# (DPM++2M/Karras/--mmap segfault on this M4 — known large-matrix Metal issue).
# Outputs WebP into public/blog-images/<slug>.webp.
set -euo pipefail
SD=~/stable-diffusion.cpp/build/bin/sd-cli
MODEL=~/sd-models/juggernautXL_v9.safetensors
OUT=/Users/jamestwose/Coding/tt-website/public/blog-images
TMP=/tmp/sd_blog
# Juggernaut README: heavy negatives hurt more than they help — keep it minimal.
NEG="text, watermark, signature, low quality, deformed, extra limbs"
STEPS=${STEPS:-20}
mkdir -p "$OUT" "$TMP"
# slug|prompt (prompts use Juggernaut's reliable tokens: Wildlife/Landscape
# Photography, Hyperdetailed Photography, Cinematic, natural light)
POSTS=(
"best-off-leash-areas-for-dogs-in-lagos-and-nearby|Wildlife Photography, a happy dog running off-leash on a wide open sandy path through a Portuguese pine forest, dappled golden sunlight, Hyperdetailed Photography, natural light"
"how-to-choose-the-right-walk-for-your-dog-based-on-their-personality-and-needs|Wildlife Photography, a dog owner walking a calm happy dog on a quiet countryside dirt path at sunrise, soft warm light, Hyperdetailed Photography"
"how-to-give-a-reactive-dog-a-fulfilling-life-without-avoiding-the-world|Wildlife Photography, a calm dog on a quiet empty woodland trail with plenty of space, soft natural morning light, peaceful, Hyperdetailed Photography"
"off-leash-dog-rules-in-portugal-what-you-need-to-know-in-2026|Landscape Photography, a dog on a lead beside a coastal cliff path in Portugal with a blue ocean and sky, summer, Hyperdetailed Photography, Cinematic"
"the-importance-of-variety-in-your-dogs-life-and-how-to-do-it-right|Wildlife Photography, a curious dog discovering a new forest trail, sniffing the ground, vibrant green foliage, Hyperdetailed Photography, natural light"
"top-10-hikes-to-do-with-your-dog-in-lagos-and-the-algarve|Landscape Photography, a hiker with a dog on a coastal cliff trail in the Algarve Portugal, dramatic golden ocean views at sunset, Hyperdetailed Photography, Cinematic"
"walking-your-dog-in-the-algarve-hidden-dangers-every-owner-should-know|Wildlife Photography, a dog walking through a sunlit pine forest in Portugal, dappled light, natural mood, Hyperdetailed Photography"
)
i=0
for entry in "${POSTS[@]}"; do
slug="${entry%%|*}"
prompt="${entry##*|}"
i=$((i+1))
png="$TMP/$slug.png"
webp="$OUT/$slug.webp"
echo "[$i/${#POSTS[@]}] $slug (steps=$STEPS)"
"$SD" -m "$MODEL" -p "$prompt" -n "$NEG" \
-H 1024 -W 1024 --steps "$STEPS" --cfg-scale 5.0 --rng std_default \
-s $((i*1000)) -o "$png" >/tmp/sd_blog_$slug.log 2>&1 || {
echo " ✗ generation failed (see /tmp/sd_blog_$slug.log)"; exit 1; }
cwebp -quiet -resize 1280 0 -q 82 "$png" -o "$webp" 2>/dev/null
echo " -> $(ls -lh "$webp" | awk '{print $5}') $webp"
done
echo "✅ done"
+64
View File
@@ -0,0 +1,64 @@
/**
* Generate sitemap.xml for TailTrails.
* Run: node scripts/generate-sitemap.mjs
* Output: public/sitemap.xml (copied to dist/ by the subsequent vite build)
*
* Blog slugs are auto-discovered from src/blogs/posts/*.tsx so the sitemap
* can't drift from the actual posts.
*/
import fs from "fs";
import path from "path";
import { fileURLToPath } from "url";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const SITE_URL = "https://tailtrails.club";
const POSTS_DIR = path.resolve(__dirname, "..", "src", "blogs", "posts");
const TODAY = new Date().toISOString().split("T")[0];
const staticRoutes = [
{ path: "/", priority: "1.0", changefreq: "weekly" },
{ path: "/blog", priority: "0.9", changefreq: "weekly" },
{ path: "/compare", priority: "0.9", changefreq: "weekly" },
{ path: "/compare/dog-walking-apps-compared", priority: "0.8", changefreq: "monthly" },
{ path: "/compare/tailtrails-vs-wooftrax", priority: "0.7", changefreq: "monthly" },
{ path: "/compare/tailtrails-vs-tractive", priority: "0.7", changefreq: "monthly" },
{ path: "/compare/tailtrails-vs-alltrails", priority: "0.7", changefreq: "monthly" },
{ path: "/compare/tailtrails-vs-fitbark", priority: "0.7", changefreq: "monthly" },
{ path: "/compare/tailtrails-vs-pitpat", priority: "0.7", changefreq: "monthly" },
{ path: "/privacy", priority: "0.3", changefreq: "yearly" },
{ path: "/terms", priority: "0.3", changefreq: "yearly" },
{ path: "/forget-me", priority: "0.3", changefreq: "yearly" },
{ path: "/finalize-forget-me", priority: "0.3", changefreq: "yearly" },
// /download is a redirect stub — not indexed, omitted from sitemap.
];
const blogSlugs = fs.existsSync(POSTS_DIR)
? fs.readdirSync(POSTS_DIR).filter((f) => f.endsWith(".tsx")).map((f) => f.replace(/\.tsx$/, ""))
: [];
const blogRoutes = blogSlugs.map((slug) => ({
path: `/blog/${slug}`,
priority: "0.7",
changefreq: "monthly",
}));
const allRoutes = [...staticRoutes, ...blogRoutes];
const xmlUrls = allRoutes
.map(
(r) => ` <url>
<loc>${SITE_URL}${r.path}</loc>
<lastmod>${TODAY}</lastmod>
<changefreq>${r.changefreq}</changefreq>
<priority>${r.priority}</priority>
</url>`
)
.join("\n");
const sitemap = `<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
${xmlUrls}
</urlset>`;
const outputPath = path.join(__dirname, "..", "public", "sitemap.xml");
fs.writeFileSync(outputPath, sitemap, "utf8");
console.log(`✅ Sitemap generated: ${outputPath} (${allRoutes.length} URLs)`);
+211
View File
@@ -0,0 +1,211 @@
#!/usr/bin/env node
/**
* Pre-render script for TailTrails
*
* Renders every route in Puppeteer so crawlers see full HTML content
* (blogs, landing, legal pages), and injects a unique <title> +
* <meta name="description"> per page for SEO.
*
* Usage: node scripts/prerender.mjs
* Runs as part of: npm run build
*/
import puppeteer from "puppeteer";
import { createServer } from "http";
import { fileURLToPath } from "url";
import path from "path";
import fs from "fs";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const DIST_DIR = path.resolve(__dirname, "..", "dist");
const POSTS_DIR = path.resolve(__dirname, "..", "src", "blogs", "posts");
const CONCURRENCY = Number(process.env.PRERENDER_CONCURRENCY) || 6;
// ── Static routes ────────────────────────────────────────────────────────────
const STATIC_ROUTES = [
"/", "/blog", "/compare",
"/compare/dog-walking-apps-compared",
"/compare/tailtrails-vs-wooftrax",
"/compare/tailtrails-vs-tractive",
"/compare/tailtrails-vs-alltrails",
"/compare/tailtrails-vs-fitbark",
"/compare/tailtrails-vs-pitpat",
"/privacy", "/terms",
"/forget-me", "/finalize-forget-me",
];
// ── Per-route SEO metadata ───────────────────────────────────────────────────
const DEFAULT_TITLE = "TailTrails — Life Unleashed";
const DEFAULT_DESC =
"Discover new trails that match your dog's needs, and turn everyday walks into real adventures. Download TailTrails on iOS and Android.";
const STATIC_SEO = {
"/": { title: DEFAULT_TITLE, desc: DEFAULT_DESC },
"/blog": {
title: "TailTrails Blog — Dog Walking Tips, Trails & Guides",
desc: "Guides on choosing the right walks for your dog, off-leash rules in Portugal, hidden dangers, and the best hikes in the Algarve.",
},
"/privacy": { title: "Privacy Policy — TailTrails", desc: "How TailTrails handles your data and privacy." },
"/terms": { title: "Terms & Conditions — TailTrails", desc: "The terms and conditions for using TailTrails." },
"/forget-me": { title: "Delete Your Account — TailTrails", desc: "Request deletion of your TailTrails account and data." },
"/finalize-forget-me": { title: "Confirm Account Deletion — TailTrails", desc: "Confirm deletion of your TailTrails account." },
"/download": { title: "Download TailTrails — iOS & Android", desc: "Get TailTrails on the App Store or Google Play." },
};
// ── Discover blog slugs + metadata from src/blogs/posts/*.tsx ────────────────
// ponytail: read slugs from the filesystem so the prerender list can't drift
// from the actual posts. Meta is regex-pulled from each file's `export const meta`.
function discoverPosts() {
if (!fs.existsSync(POSTS_DIR)) return [];
return fs.readdirSync(POSTS_DIR)
.filter((f) => f.endsWith(".tsx"))
.map((f) => {
const slug = f.replace(/\.tsx$/, "");
const src = fs.readFileSync(path.join(POSTS_DIR, f), "utf-8");
const title = src.match(/title:\s*'([^']*)'/)?.[1];
const description = src.match(/description:\s*'([^']*)'/)?.[1];
return { slug, title, description };
});
}
const posts = discoverPosts();
const BLOG_ROUTES = posts.map((p) => `/blog/${p.slug}`);
// ── Static file server (SPA fallback) ────────────────────────────────────────
function startServer(port) {
const mimes = {
".html": "text/html", ".js": "application/javascript", ".mjs": "application/javascript",
".css": "text/css", ".json": "application/json", ".png": "image/png", ".jpg": "image/jpeg",
".jpeg": "image/jpeg", ".gif": "image/gif", ".svg": "image/svg+xml", ".ico": "image/x-icon",
".woff": "font/woff", ".woff2": "font/woff2", ".ttf": "font/ttf", ".webp": "image/webp",
".avif": "image/avif", ".mp4": "video/mp4", ".txt": "text/plain",
};
return new Promise((resolve, reject) => {
const server = createServer((req, res) => {
let fp = path.join(DIST_DIR, req.url === "/" ? "index.html" : req.url);
if (!fs.existsSync(fp) || fs.statSync(fp).isDirectory()) fp = path.join(DIST_DIR, "index.html");
fs.readFile(fp, (err, data) => {
if (err) { res.writeHead(404); res.end("Not found"); return; }
res.writeHead(200, { "Content-Type": mimes[path.extname(fp)] || "application/octet-stream" });
res.end(data);
});
});
server.listen(port, () => resolve(server));
server.on("error", reject);
});
}
// ── Fallback-only SEO injection ─────────────────────────────────────────────
// Every prerendered route now mounts a <SEO> component that sets <title>,
// description, OG, Twitter, canonical and JSON-LD during the Puppeteer render.
// This only fills in the gaps if a page forgot to mount <SEO> (title still the
// index.html placeholder "Tail Trails", or no description meta present).
function injectSeo(html, title, desc) {
let out = html;
if (title && /<title>Tail Trails<\/title>/i.test(out)) {
out = out.replace(/<title>Tail Trails<\/title>/i, `<title>${title}</title>`);
}
if (desc && !/<meta\s+name="description"\s+content="[^"]*"/i.test(out)) {
out = out.replace("<head>", `<head><meta name="description" content="${desc}">`);
}
return out;
}
function seoFor(route) {
if (STATIC_SEO[route]) return STATIC_SEO[route];
const post = route.startsWith("/blog/") ? posts.find((p) => `/blog/${p.slug}` === route) : null;
if (post) {
return {
title: post.title ? `${post.title} — TailTrails Blog` : "TailTrails Blog",
desc: post.description || DEFAULT_DESC,
};
}
return { title: DEFAULT_TITLE, desc: DEFAULT_DESC };
}
// ── Render one page ──────────────────────────────────────────────────────────
async function prerenderRoute(browser, url, route) {
const page = await browser.newPage();
try {
await page.goto(url, { waitUntil: "networkidle0", timeout: 20000 });
await page.waitForFunction(
() => { const r = document.getElementById("root"); return r && r.children.length > 0; },
{ timeout: 10000 }
);
// let animations / images settle
await new Promise((r) => setTimeout(r, 400));
let html = await page.content();
const { title, desc } = seoFor(route);
html = injectSeo(html, title, desc);
const outDir = route === "/" ? DIST_DIR : path.join(DIST_DIR, route);
fs.mkdirSync(outDir, { recursive: true });
fs.writeFileSync(path.join(outDir, "index.html"), html, "utf-8");
return { route, ok: true };
} catch (err) {
return { route, ok: false, error: err.message };
} finally {
await page.close();
}
}
// ── Parallel batch renderer ──────────────────────────────────────────────────
async function renderBatch(browser, routes, baseUrl, concurrency) {
let done = 0;
const total = routes.length;
const errors = [];
for (let i = 0; i < total; i += concurrency) {
const batch = routes.slice(i, i + concurrency);
const results = await Promise.all(batch.map((r) => prerenderRoute(browser, `${baseUrl}${r}`, r)));
for (const r of results) {
done++;
r.ok
? console.log(` ✓ [${done}/${total}] ${r.route}`)
: console.error(` ✗ [${done}/${total}] ${r.route}: ${r.error}`);
if (!r.ok) errors.push(r.route);
}
}
return errors;
}
// ── Main ─────────────────────────────────────────────────────────────────────
async function main() {
if (!fs.existsSync(DIST_DIR)) {
console.error("Error: dist/ not found. Run 'vite build' first.");
process.exit(1);
}
console.log("🔄 Pre-rendering pages for SEO...\n");
console.log(` Discovered ${posts.length} blog post(s): ${posts.map((p) => p.slug).join(", ")}\n`);
const routes = [...STATIC_ROUTES, ...BLOG_ROUTES];
const PORT = 4823;
const BASE = `http://localhost:${PORT}`;
const server = await startServer(PORT);
console.log(` Serving dist/ on ${BASE}`);
console.log(` Rendering ${routes.length} pages in Puppeteer (concurrency ${CONCURRENCY})...\n`);
const browser = await puppeteer.launch({
headless: true,
args: ["--no-sandbox", "--disable-setuid-sandbox", "--disable-gpu"],
});
const errors = await renderBatch(browser, routes, BASE, CONCURRENCY);
await browser.close();
server.close();
console.log(`\n✅ Pre-rendered ${routes.length} pages.`);
if (errors.length) console.log(` ⚠️ ${errors.length} route(s) failed: ${errors.join(", ")}`);
// Verify key pages
console.log("\n📋 Verifying key pages...\n");
const verify = ["/", "/blog", ...(BLOG_ROUTES.length ? [BLOG_ROUTES[0]] : [])];
for (const route of verify) {
const fp = route === "/" ? path.join(DIST_DIR, "index.html") : path.join(DIST_DIR, route, "index.html");
if (!fs.existsSync(fp)) { console.log(` ⚠ ${route}: not found`); continue; }
const html = fs.readFileSync(fp, "utf-8");
const title = html.match(/<title>(.*?)<\/title>/)?.[1] || "MISSING";
const desc = html.match(/<meta\s+name="description"\s+content="([^"]+)"/)?.[1]?.slice(0, 70) || "MISSING";
const rootHasContent = html.includes("Every walk") || html.includes("post-content") || html.includes("Latest Articles");
console.log(` ${route}\n Title: ${title}\n Desc: ${desc}…\n Content in HTML: ${rootHasContent}\n`);
}
}
main().catch((err) => { console.error("Pre-rendering failed:", err); process.exit(1); });
+32 -3
View File
@@ -1,32 +1,61 @@
import { BrowserRouter, Routes, Route } from "react-router-dom";
import { BrowserRouter, Routes, Route, useLocation } from "react-router-dom";
import { useEffect } from "react";
import Index from "./pages/Index";
import Privacy from "./pages/Privacy";
import Terms from "./pages/Terms";
import NotFound from "./pages/NotFound";
import Waitlist from "./pages/Waitlist";
import RequestAccountDeletion from "./pages/RequestAccountDeletion";
import FinalizeAccountDeletion from "./pages/FinalizeAccountDeletion";
import Download from "./pages/Download";
import BlogList from "./pages/BlogList";
import BlogPost from "./pages/BlogPost";
import Compare from "./pages/Compare";
import BigComparison from "./pages/compare/dog-walking-apps-compared";
import TailtrailsVsWooftrax from "./pages/compare/tailtrails-vs-wooftrax";
import TailtrailsVsTractive from "./pages/compare/tailtrails-vs-tractive";
import TailtrailsVsAlltrails from "./pages/compare/tailtrails-vs-alltrails";
import TailtrailsVsFitbark from "./pages/compare/tailtrails-vs-fitbark";
import TailtrailsVsPitpat from "./pages/compare/tailtrails-vs-pitpat";
import CookieConsent from "./components/ui/CookieConsent";
import { ToastProvider, ToastViewport } from "./components/ui/toast";
import Toaster from "./components/ui/Toaster";
// Client-side route changes don't trigger the browser's native anchor scroll.
const ScrollToHash = () => {
const { pathname, hash } = useLocation();
useEffect(() => {
if (!hash) { window.scrollTo(0, 0); return; }
// wait for the destination page to render before scrolling
requestAnimationFrame(() => {
const el = document.getElementById(hash.slice(1));
if (el) { el.scrollIntoView({ behavior: "smooth" }); }
else { window.scrollTo(0, 0); }
});
}, [pathname, hash]);
return null;
};
const App = () => (
<>
<ToastProvider>
<BrowserRouter>
<ScrollToHash />
<Routes>
<Route path="/" element={<Index />} />
<Route path="/privacy" element={<Privacy />} />
<Route path="/terms" element={<Terms />} />
<Route path="/waitlist" element={<Waitlist />} />
<Route path="/forget-me" element={<RequestAccountDeletion />} />
<Route path="/finalize-forget-me" element={<FinalizeAccountDeletion />} />
<Route path="/download" element={<Download />} />
<Route path="/blog" element={<BlogList />} />
<Route path="/blog/:slug" element={<BlogPost />} />
<Route path="/compare" element={<Compare />} />
<Route path="/compare/dog-walking-apps-compared" element={<BigComparison />} />
<Route path="/compare/tailtrails-vs-wooftrax" element={<TailtrailsVsWooftrax />} />
<Route path="/compare/tailtrails-vs-tractive" element={<TailtrailsVsTractive />} />
<Route path="/compare/tailtrails-vs-alltrails" element={<TailtrailsVsAlltrails />} />
<Route path="/compare/tailtrails-vs-fitbark" element={<TailtrailsVsFitbark />} />
<Route path="/compare/tailtrails-vs-pitpat" element={<TailtrailsVsPitpat />} />
{/* ADD ALL CUSTOM ROUTES ABOVE THE CATCH-ALL "*" ROUTE */}
<Route path="*" element={<NotFound />} />
</Routes>
+42
View File
@@ -0,0 +1,42 @@
import type { ComponentType } from 'react';
import bestOffLeash, { meta as bestOffLeashMeta } from './posts/best-off-leash-areas-for-dogs-in-lagos-and-nearby';
import howToChoose, { meta as howToChooseMeta } from './posts/how-to-choose-the-right-walk-for-your-dog-based-on-their-personality-and-needs';
import reactiveDog, { meta as reactiveDogMeta } from './posts/how-to-give-a-reactive-dog-a-fulfilling-life-without-avoiding-the-world';
import offLeashRules, { meta as offLeashRulesMeta } from './posts/off-leash-dog-rules-in-portugal-what-you-need-to-know-in-2026';
import variety, { meta as varietyMeta } from './posts/the-importance-of-variety-in-your-dogs-life-and-how-to-do-it-right';
import topHikes, { meta as topHikesMeta } from './posts/top-10-hikes-to-do-with-your-dog-in-lagos-and-the-algarve';
import algarveDangers, { meta as algarveDangersMeta } from './posts/walking-your-dog-in-the-algarve-hidden-dangers-every-owner-should-know';
export type BlogMeta = {
title: string;
date: string;
description: string;
category: string;
readTime: string;
live: boolean;
image: string;
};
export type BlogEntry = {
slug: string;
meta: BlogMeta;
Component: ComponentType;
};
// ponytail: eager imports — 7 short text posts. Drops the `marked` runtime dep
// and the fetch+parse of .md files; net bundle is smaller. Code-split per post
// only if the post count grows large.
export const posts: BlogEntry[] = [
{ slug: 'best-off-leash-areas-for-dogs-in-lagos-and-nearby', meta: bestOffLeashMeta, Component: bestOffLeash },
{slug: 'how-to-choose-the-right-walk-for-your-dog-based-on-their-personality-and-needs', meta: howToChooseMeta, Component: howToChoose},
{slug: 'how-to-give-a-reactive-dog-a-fulfilling-life-without-avoiding-the-world', meta: reactiveDogMeta, Component: reactiveDog},
{slug: 'off-leash-dog-rules-in-portugal-what-you-need-to-know-in-2026', meta: offLeashRulesMeta, Component: offLeashRules},
{slug: 'the-importance-of-variety-in-your-dogs-life-and-how-to-do-it-right', meta: varietyMeta, Component: variety},
{slug: 'top-10-hikes-to-do-with-your-dog-in-lagos-and-the-algarve', meta: topHikesMeta, Component: topHikes},
{slug: 'walking-your-dog-in-the-algarve-hidden-dangers-every-owner-should-know', meta: algarveDangersMeta, Component: algarveDangers},
].sort((a, b) => b.meta.date.localeCompare(a.meta.date));
export const getAllPosts = (): BlogEntry[] => posts;
export const getPostBySlug = (slug: string): BlogEntry | undefined =>
posts.find((p) => p.slug === slug);
@@ -0,0 +1,130 @@
import type { BlogMeta } from '../index';
export const meta: BlogMeta = {
title: 'Best Off-Leash Areas for Dogs in Lagos (and Nearby)',
date: '2024-05-01',
description: 'Looking for off-leash areas for dogs in Lagos, Portugal? Discover the best spots for safe, enjoyable off-leash walks, plus what to watch out for.',
category: 'Insights',
readTime: '6 min',
live: true,
image: '/blog-images/best-off-leash-areas-for-dogs-in-lagos-and-nearby.webp',
};
export default function Post() {
return (
<>
<h3>Introduction</h3>
<p>Finding a good place to let your dog off-leash in Lagos isn't always straightforward.</p>
<p>Some areas are:</p>
<ul>
<li>Too busy</li>
<li>Too unpredictable</li>
<li>Or not actually suitable for your dog</li>
</ul>
<p>But when you find the right spot, it changes everything.</p>
<p>Here are some of the best off-leash-friendly areas around Lagos, and how to choose the right one for your dog.</p>
<hr />
<h2>1. Mata Nacional de Barão de São João</h2>
<p><strong>Best for:</strong> Most dogs</p>
<p><strong>Why it works:</strong></p>
<ul>
<li>Wide, open forest trails</li>
<li>Plenty of space to create distance</li>
<li>Shaded and cooler</li>
</ul>
<p>Ideal for dogs who need <strong>space without isolation</strong></p>
<hr />
<h2>2. Praia da Bordeira (West Coast)</h2>
<p><strong>Best for:</strong> High-energy dogs</p>
<p><strong>Why it works:</strong></p>
<ul>
<li>Huge open beach</li>
<li>Low crowd density (outside peak times)</li>
<li>Great for running</li>
</ul>
<p>Watch:</p>
<ul>
<li>Wind exposure</li>
<li>Seasonal rules</li>
</ul>
<hr />
<h2>3. Ria de Alvor (Quiet Sections)</h2>
<p><strong>Best for:</strong> Lower-energy or calm dogs</p>
<p><strong>Why it works:</strong></p>
<ul>
<li>Flat terrain</li>
<li>Open visibility</li>
</ul>
<p>Avoid peak hours, it can get busy</p>
<hr />
<h2>4. Monchique Countryside</h2>
<p><strong>Best for:</strong> Dogs needing low stimulation</p>
<p><strong>Why it works:</strong></p>
<ul>
<li>Quiet</li>
<li>Natural</li>
<li>Cooler temperatures</li>
</ul>
<p>Great for reactive or sensitive dogs</p>
<hr />
<h2>5. Local Countryside Trails (Hidden Gems)</h2>
<p>Often overlooked, but some of the best options.</p>
<p>These areas offer:</p>
<ul>
<li>Lower dog traffic</li>
<li>More control</li>
<li>Flexible space</li>
</ul>
<p>Especially useful for:</p>
<ul>
<li>Training recall</li>
<li>Building confidence</li>
</ul>
<p>Check out trails near countryside villages like Bensafrim and Vila do Bispo.</p>
<hr />
<h2>Important: Not Every "Off-Leash Area" Is Right</h2>
<p>Just because a place allows off-leash doesn't mean it's suitable.</p>
<p>Before letting your dog off:</p>
<ul>
<li>Check the environment</li>
<li>Look at dog traffic</li>
<li>Consider your dog's behaviour</li>
</ul>
<hr />
<h2>What Makes a Good Off-Leash Spot?</h2>
<p>The best places have:</p>
<ul>
<li>Space to move freely</li>
<li>Clear visibility</li>
<li>Low unpredictability</li>
<li>An environment suited to your dog</li>
</ul>
<hr />
<h2>Final Thoughts</h2>
<p>The goal isn't just to let your dog off-leash.</p>
<p>It's to do it in a way that:</p>
<ul>
<li>Feels safe</li>
<li>Feels calm</li>
<li>And actually benefits your dog</li>
</ul>
<p>Because the right environment makes all the difference.</p>
</>
);
}
@@ -0,0 +1,127 @@
import type { BlogMeta } from '../index';
export const meta: BlogMeta = {
title: 'How to Choose the Right Walk for Your Dog (Based on Their Personality and Needs)',
date: '2024-04-20',
description: 'Not all walks are equal. Learn how to choose the right environment for your dog based on energy, temperament, and needs.',
category: 'Strategy',
readTime: '5 min',
live: true,
image: '/blog-images/how-to-choose-the-right-walk-for-your-dog-based-on-their-personality-and-needs.webp',
};
export default function Post() {
return (
<>
<h3>Introduction</h3>
<p>Choosing a walk for your dog often comes down to convenience.</p>
<p>What's nearby. What you've done before. What seems "good enough."</p>
<p>But the best walks aren't chosen that way.</p>
<p>They're chosen based on <strong>your dog</strong>.</p>
<hr />
<h3>Step 1: Understand Your Dog's Profile</h3>
<p>Start with the basics:</p>
<ul>
<li>Energy level (low, medium, high)</li>
<li>Sociability (loves dogs vs prefers space)</li>
<li>Confidence (bold vs cautious)</li>
<li>Sensitivities (noise, movement, crowds)</li>
<li>Physical needs (age, injuries, endurance)</li>
</ul>
<p>This becomes your baseline.</p>
<hr />
<h3>Step 2: Match the Environment</h3>
<p>Now consider the walk itself:</p>
<h4>High-energy dogs</h4>
<p>Need:</p>
<ul>
<li>Long routes</li>
<li>Open space</li>
<li>Opportunities to run</li>
</ul>
<p>Best environments:</p>
<ul>
<li>Wide trails</li>
<li>Open countryside</li>
<li>Low restriction areas</li>
</ul>
<p><strong>Sensitive or reactive dogs</strong></p>
<p>Need:</p>
<ul>
<li>Predictability</li>
<li>Space from others</li>
<li>Low stimulation</li>
</ul>
<p>Best environments:</p>
<ul>
<li>Quiet trails</li>
<li>Low dog traffic areas</li>
<li>Wide paths with visibility</li>
</ul>
<h4>Older or recovering dogs</h4>
<p>Need:</p>
<ul>
<li>Shorter distances</li>
<li>Even terrain</li>
<li>Shade</li>
</ul>
<p>Best environments:</p>
<ul>
<li>Flat trails</li>
<li>Forest paths</li>
<li>Low-exertion routes</li>
</ul>
<hr />
<h3>Step 3: Look Beyond the Surface</h3>
<p>A trail might look perfect, but details matter:</p>
<ul>
<li>Is it narrow or open?</li>
<li>Is there shade?</li>
<li>Are dogs typically off-leash there?</li>
<li>Is it busy at certain times?</li>
</ul>
<p>These small factors make a big difference.</p>
<hr />
<h3>Step 4: Pay Attention to Feedback</h3>
<p>Your dog will tell you if it's working.</p>
<p>Look for:</p>
<ul>
<li>Relaxed body language</li>
<li>Natural curiosity</li>
<li>Steady energy (not frantic or shut down)</li>
<li>Willing engagement with surroundings</li>
</ul>
<p>If you're seeing stress signals, it may not be the right fit.</p>
<hr />
<h3>Step 5: Prioritise Variety Intentionally</h3>
<p>Variety is important.</p>
<p>But random variety isn't helpful.</p>
<p>Instead:</p>
<ul>
<li>Rotate between environments that suit your dog</li>
<li>Gradually introduce new challenges</li>
<li>Build confidence over time</li>
</ul>
<hr />
<h3>Final Thoughts</h3>
<p>The best walk isn't the longest or the most popular.</p>
<p>It's the one that fits your dog.</p>
<p>Because when you choose the right environment, you're not just going for a walk, you're creating a better experience for both of you.</p>
</>
);
}
@@ -0,0 +1,126 @@
import type { BlogMeta } from '../index';
export const meta: BlogMeta = {
title: 'How to Give a Reactive Dog a Fulfilling Life (Without Avoiding the World)',
date: '2024-03-15',
description: 'Have a reactive dog? Learn how to give them a fulfilling life with the right environments, structure, and approach.',
category: 'Insights',
readTime: '7 min',
live: true,
image: '/blog-images/how-to-give-a-reactive-dog-a-fulfilling-life-without-avoiding-the-world.webp',
};
export default function Post() {
return (
<>
<h3>Introduction</h3>
<p>Living with a reactive dog can feel limiting.</p>
<p>You might:</p>
<ul>
<li>Avoid certain places</li>
<li>Walk at odd hours</li>
<li>Stick to the same safe routes</li>
</ul>
<p>But here's the truth:</p>
<p>Your dog can still have a full, enriching life.</p>
<p>It just requires a different approach.</p>
<hr />
<h2>What Reactivity Often Means</h2>
<p>Reactivity isn't "bad behaviour."</p>
<p>It's usually:</p>
<ul>
<li>Overstimulation</li>
<li>Fear or uncertainty</li>
<li>Lack of space</li>
<li>Too much, too quickly</li>
</ul>
<hr />
<h2>Environment Matters More Than Training Alone</h2>
<p>Training is important, but environment is foundational.</p>
<p>Even a well-trained dog can struggle in:</p>
<ul>
<li>Busy areas</li>
<li>Narrow paths</li>
<li>High dog-traffic zones</li>
</ul>
<hr />
<h2>Choosing the Right Environments</h2>
<p>Look for places that offer:</p>
<ul>
<li>Space to move away</li>
<li>Predictability</li>
<li>Lower levels of stimulation</li>
</ul>
<p>Examples:</p>
<ul>
<li>Wide trails</li>
<li>Quiet countryside</li>
<li>Off-peak walking times</li>
</ul>
<hr />
<h2>Give Them the Right Kind of Freedom</h2>
<p>Reactive dogs don't need:</p>
<ul>
<li>More pressure</li>
<li>More exposure too quickly</li>
</ul>
<p>They need:</p>
<ul>
<li>Controlled experiences</li>
<li>Space to decompress</li>
<li>Opportunities to succeed</li>
</ul>
<hr />
<h2>Common Mistakes</h2>
<ul>
<li>Taking them to busy "dog-friendly" areas</li>
<li>Forcing social interactions</li>
<li>Ignoring early stress signals</li>
</ul>
<hr />
<h2>Building Confidence Over Time</h2>
<p>Start small.</p>
<ul>
<li>Familiar environments</li>
<li>Gradual changes</li>
<li>Positive reinforcement</li>
</ul>
<p>Confidence grows through:</p>
<p>Consistent, successful experiences</p>
<hr />
<h2>Fulfilment Looks Different</h2>
<p>A fulfilling life doesn't mean:</p>
<ul>
<li>Busy parks</li>
<li>Social environments</li>
</ul>
<p>It means:</p>
<ul>
<li>Calm exploration</li>
<li>Comfortable movement</li>
<li>Feeling safe in the world</li>
</ul>
<hr />
<h2>Final Thoughts</h2>
<p>Your dog doesn't need to handle everything.</p>
<p>They just need the <strong>right environments to thrive in</strong>.</p>
<p>And when you give them that, everything starts to shift.</p>
</>
);
}
@@ -0,0 +1,120 @@
import type { BlogMeta } from '../index';
export const meta: BlogMeta = {
title: 'Off-Leash Dog Rules in Portugal: What You Need to Know in 2026',
date: '2026-04-01',
description: 'Can you walk your dog off-leash in Portugal? Learn the rules, risks, and best practices for safe and legal off-leash walks.',
category: 'Trends',
readTime: '6 min',
live: true,
image: '/blog-images/off-leash-dog-rules-in-portugal-what-you-need-to-know-in-2026.webp',
};
export default function Post() {
return (
<>
<h3>Introduction</h3>
<p>Letting your dog off-leash is one of the simplest ways to give them freedom, enrichment, and a more natural experience.</p>
<p>But if you're living in or visiting Portugal, the rules aren't always obvious, and getting it wrong can lead to fines, unsafe situations, or conflict with others.</p>
<p>Here's what you need to know.</p>
<hr />
<h3>Are Dogs Allowed Off-Leash in Portugal?</h3>
<p>In general, dogs in Portugal are expected to be <strong>under control at all times in public spaces</strong>.</p>
<p>That doesn't always mean physically on a leash - but it <em>does</em> mean:</p>
<ul>
<li>Your dog must respond reliably to commands</li>
<li>They must not pose a risk to people, other animals, or property</li>
<li>They must not cause disturbance</li>
</ul>
<p>In many <strong>urban areas</strong>, leash use is effectively required.</p>
<p>In more <strong>rural or remote areas</strong>, off-leash walking is more commonly accepted - but still comes with responsibility.</p>
<hr />
<h3>Official Rules, Restrictions, and Fines</h3>
<p>In Portugal, there are also specific legal restrictions to be aware of, especially around beaches and public spaces. During the summer season (typically June to September), many beaches prohibit dogs entirely, regardless of whether they are on or off-leash. These rules are set by local municipalities and are usually clearly signposted at beach entrances. Outside of summer, some beaches become dog-friendly again, but leash expectations can still apply.</p>
<p>Failing to follow these rules can result in on-the-spot fines, which typically range from around €25 up to €250 or more, depending on the situation and location. Additional penalties may apply if a dog is considered out of control, causes disturbance, or poses a risk to others.</p>
<p>Because enforcement and rules can vary by region, it's always worth checking local signage before letting your dog off-leash, especially in popular or seasonal areas.</p>
<hr />
<h3>Where Off-Leash Walking Is (Usually) Acceptable</h3>
<p>You're more likely to safely and responsibly walk your dog off-leash in:</p>
<ul>
<li>Quiet countryside trails</li>
<li>Forest paths with low foot traffic</li>
<li>Remote coastal routes (away from crowds)</li>
<li>Open land where visibility is high</li>
</ul>
<p>However, even in these areas, it's not just about permission - it's about suitability for your dog.</p>
<hr />
<h3>Where You Should Keep Your Dog On-Leash</h3>
<p>There are several environments where off-leash walking is <strong>not recommended or restricted</strong>:</p>
<ul>
<li>Busy beaches (especially during peak season)</li>
<li>Urban parks and promenades</li>
<li>Narrow cliff paths</li>
<li>Areas with livestock or wildlife</li>
<li>High dog-traffic zones</li>
</ul>
<p>Even if technically allowed, these environments can be:</p>
<ul>
<li>Overstimulating</li>
<li>Unsafe</li>
<li>Stressful for reactive or sensitive dogs</li>
</ul>
<hr />
<h3>The Risks Most Dog Owners Overlook</h3>
<p>Many owners assume off-leash = freedom.</p>
<p>But without the right environment, it can quickly become the opposite.</p>
<p>Common risks include:</p>
<ul>
<li>Uncontrolled interactions with other dogs</li>
<li>Encounters with wildlife or livestock</li>
<li>Sudden terrain hazards (cliffs, unstable ground)</li>
<li>Overheating in exposed areas</li>
<li>Poor recall in unfamiliar settings</li>
</ul>
<p>The result? Stress for both you <em>and</em> your dog.</p>
<hr />
<h3>Why "Dog-Friendly" Isn't Enough</h3>
<p>A beach might allow dogs. A trail might be popular with dog walkers.</p>
<p>But that doesn't mean it's <strong>right for your dog</strong>.</p>
<p>Every dog has different needs:</p>
<ul>
<li>Some thrive in social, high-energy spaces</li>
<li>Others need quiet, predictable environments</li>
<li>Some need open space to run</li>
<li>Others need structured, low-stimulation walks</li>
</ul>
<p>Choosing the wrong environment can undo the very thing you're trying to give them.</p>
<hr />
<h3>How to Choose the Right Off-Leash Environment</h3>
<p>Before letting your dog off-leash, ask:</p>
<ul>
<li>How busy is this area?</li>
<li>Are there other dogs nearby?</li>
<li>Is the terrain safe and visible?</li>
<li>Is there wildlife or livestock?</li>
<li>Will my dog feel confident here, or overwhelmed?</li>
</ul>
<p>The goal isn't just freedom. It's <strong>setting your dog up for a good time</strong>.</p>
<hr />
<h3>Final Thoughts</h3>
<p>Walking your dog off-leash in Portugal can be an awesome experience, when it's done in the right place, at the right time, for the right dog.</p>
<p>Because it's not just about whether dogs are allowed. It's about whether the environment actually fits.</p>
</>
);
}
@@ -0,0 +1,116 @@
import type { BlogMeta } from '../index';
export const meta: BlogMeta = {
title: 'The Importance of Variety in Your Dog’s Life (And How to Do It Right)',
date: '2024-02-28',
description: 'Dogs need more than the same daily walk. Learn why variety matters and how to safely introduce new environments into your dog’s routine.',
category: 'Strategy',
readTime: '5 min',
live: true,
image: '/blog-images/the-importance-of-variety-in-your-dogs-life-and-how-to-do-it-right.webp',
};
export default function Post() {
return (
<>
<h3>Introduction</h3>
<p>Most dogs live very predictable lives.</p>
<p>The same route. The same pace. The same environment.</p>
<p>And while routine has its place, too much of it can limit your dog's experience of the world.</p>
<hr />
<h2>Why Variety Matters</h2>
<p>Dogs are naturally curious.</p>
<p>They're wired to:</p>
<ul>
<li>Explore</li>
<li>Smell</li>
<li>Navigate new environments</li>
</ul>
<p>When every walk is the same, they miss out on:</p>
<ul>
<li>Mental stimulation</li>
<li>Physical engagement</li>
<li>Confidence-building experiences</li>
</ul>
<hr />
<h2>It's Not Just About Exercise</h2>
<p>A longer walk doesn't always mean a better walk.</p>
<p>A short walk in a new environment can be:</p>
<ul>
<li>More stimulating</li>
<li>More engaging</li>
<li>More fulfilling</li>
</ul>
<hr />
<h2>The Benefits of Variety</h2>
<p>Introducing new environments can:</p>
<ul>
<li>Reduce boredom</li>
<li>Improve behaviour</li>
<li>Build confidence</li>
<li>Strengthen your bond</li>
</ul>
<hr />
<h2>But Random Variety Isn't the Answer</h2>
<p>This is where many owners get it wrong.</p>
<p>Throwing your dog into:</p>
<ul>
<li>Busy environments</li>
<li>Unpredictable situations</li>
<li>Overstimulating spaces</li>
</ul>
<p>Can actually:</p>
<ul>
<li>Increase stress</li>
<li>Lead to reactivity</li>
<li>Create negative experiences</li>
</ul>
<hr />
<h2>How to Introduce Variety Properly</h2>
<h3>1. Start with similar environments</h3>
<p>If your dog is used to quiet trails, try new <em>quiet</em> trails first.</p>
<hr />
<h3>2. Change one variable at a time</h3>
<ul>
<li>New location, same energy level</li>
<li>New terrain, same level of stimulation</li>
</ul>
<hr />
<h3>3. Watch your dog's response</h3>
<p>Let their behaviour guide your decisions.</p>
<hr />
<h3>4. Build gradually</h3>
<p>Confidence grows with positive experiences.</p>
<hr />
<h2>The Goal</h2>
<p>Not constant novelty.</p>
<p>But <strong>intentional variation</strong> that suits your dog.</p>
<hr />
<h2>Final Thoughts</h2>
<p>Your dog doesn't need more walks.</p>
<p>They need <strong>better, more varied experiences</strong>.</p>
<p>Because when you get it right, even a simple walk can feel completely different.</p>
</>
);
}
@@ -0,0 +1,91 @@
import type { BlogMeta } from '../index';
export const meta: BlogMeta = {
title: 'Top 10 Hikes to do with your Dog in Lagos and the Algarve',
date: '2024-06-10',
description: 'Discover the best hikes in Lagos and the Algarve to take your dog on. From coastal trails to quiet forests, find the right walk for your dog.',
category: 'Insights',
readTime: '8 min',
live: true,
image: '/blog-images/top-10-hikes-to-do-with-your-dog-in-lagos-and-the-algarve.webp',
};
export default function Post() {
return (
<>
<h3>Introduction</h3>
<p>If you're based in or visiting Lagos, you're surrounded by some of the most beautiful walking routes in Europe.</p>
<p>But not all hikes are equal - especially when you're walking with a dog.</p>
<p>Some are:</p>
<ul>
<li>Too exposed</li>
<li>Too busy</li>
<li>Too steep</li>
<li>Or just not suited to your dog's needs</li>
</ul>
<p>Here are 10 of the best dog-friendly hikes in the Algarve, plus what makes each one a good fit.</p>
<hr />
<h2>1. Ponta da Piedade Coastal Walk</h2>
<p><strong>Best for:</strong> Calm, confident dogs</p>
<p><strong>Type:</strong> Cliffside / scenic</p>
<p>Incredible views, but:</p>
<ul>
<li>Narrow in parts</li>
<li>High foot traffic</li>
</ul>
<p>Best early morning or off-season</p>
<hr />
<h2>2. Mata Nacional de Barão de São João</h2>
<p><strong>Best for:</strong> Most dogs</p>
<p><strong>Type:</strong> Forest / shaded</p>
<ul>
<li>Wide trails</li>
<li>Plenty of shade</li>
<li>Lower stimulation</li>
</ul>
<hr />
<h2>3. Ria de Alvor Nature Reserve</h2>
<p><strong>Best for:</strong> Low to moderate energy dogs</p>
<p><strong>Type:</strong> Boardwalk / coastal</p>
<ul>
<li>Flat and accessible</li>
<li>Can be busy</li>
<li>Limited shade</li>
</ul>
<hr />
<h2>How to Choose the Right One</h2>
<p>Instead of asking:</p>
<p>"Which hike is best?"</p>
<p>Ask:</p>
<p>"Which hike is right for my dog?"</p>
<p>Consider:</p>
<ul>
<li>Energy level</li>
<li>Sociability</li>
<li>Confidence</li>
<li>Weather</li>
<li>Time of day</li>
</ul>
<hr />
<h2>Final Thoughts</h2>
<p>The Algarve is full of incredible places to explore with your dog.</p>
<p>But the best walks aren't just beautiful.</p>
<p>They're the ones that:</p>
<ul>
<li>Suit your dog</li>
<li>Feel comfortable</li>
<li>And allow them to be at their best</li>
</ul>
</>
);
}
@@ -0,0 +1,80 @@
import type { BlogMeta } from '../index';
export const meta: BlogMeta = {
title: 'Walking Your Dog in the Algarve: Hidden Dangers Every Owner Should Know',
date: '2024-07-05',
description: 'Walking your dog in the Algarve? Learn about hidden dangers like pine processionary caterpillars, grass seeds, heat, and more — plus how to keep your dog safe.',
category: 'Tutorial',
readTime: '7 min',
live: true,
image: '/blog-images/walking-your-dog-in-the-algarve-hidden-dangers-every-owner-should-know.webp',
};
export default function Post() {
return (
<>
<h3>Introduction</h3>
<p>The Algarve is one of the best places in Europe to explore with your dog.</p>
<p>Cliffside trails. Quiet forests. Open countryside.</p>
<p>But beneath all that beauty, there are a few <strong>less obvious risks</strong> that many dog owners (especially newcomers) don't see coming.</p>
<p>Knowing what to look out for can make the difference between a great walk and an emergency vet visit.</p>
<hr />
<h2>1. Pine Processionary Caterpillars (High Risk)</h2>
<p>These are one of the <strong>most dangerous hazards</strong> for dogs in Portugal.</p>
<p>They appear in:</p>
<ul>
<li>Pine forests</li>
<li>Woodland trails</li>
<li>Warmer months (typically late winter to spring)</li>
</ul>
<h3>Why they're dangerous:</h3>
<p>Their tiny hairs contain a toxin that can cause:</p>
<ul>
<li>Severe swelling of the mouth and tongue</li>
<li>Vomiting</li>
<li>Difficulty breathing</li>
<li>Tissue damage (in serious cases)</li>
</ul>
<h3>What to look for:</h3>
<ul>
<li>Caterpillars moving in a line (procession)</li>
<li>Nests in pine trees (silk-like bundles)</li>
</ul>
<hr />
<h2>2. Grass Seeds (Foxtails)</h2>
<p>Small, sharp, and incredibly problematic.</p>
<p>They're common in:</p>
<ul>
<li>Dry grasslands</li>
<li>Trail edges</li>
<li>Late spring and summer</li>
</ul>
<h3>Why they're dangerous:</h3>
<p>They can lodge in:</p>
<ul>
<li>Noses</li>
<li>Ears</li>
<li>Eyes</li>
<li>Between toes</li>
</ul>
<hr />
<h2>Final Thoughts</h2>
<p>Walking your dog in the Algarve can be one of the best parts of living or visiting here.</p>
<p>But the best walks happen when you:</p>
<ul>
<li>Know what to look out for</li>
<li>Choose environments intentionally</li>
<li>Match the walk to your dog's needs</li>
</ul>
</>
);
}
+224
View File
@@ -0,0 +1,224 @@
import { useEffect } from "react";
import { useLocation } from "react-router-dom";
import { APP_STORE_URL, PLAY_STORE_URL } from "@/lib/storeLinks";
export interface SEOProps {
title: string;
description: string;
canonical?: string;
ogImage?: string;
ogType?: string;
jsonLd?: object | object[];
noIndex?: boolean;
noIndexFollow?: boolean;
keywords?: string[];
}
const SITE_URL = "https://tailtrails.club";
const DEFAULT_OG_IMAGE = `${SITE_URL}/tailtrails-logo.png`;
export default function SEO({
title,
description,
canonical,
ogImage = DEFAULT_OG_IMAGE,
ogType = "website",
jsonLd,
noIndex = false,
noIndexFollow = false,
keywords,
}: SEOProps) {
const location = useLocation();
const fullTitle = `${title} | TailTrails`;
const canonicalUrl = canonical || `${SITE_URL}${location.pathname}`;
useEffect(() => {
document.title = fullTitle;
const setMeta = (attr: "name" | "property", key: string, content: string) => {
let el = document.querySelector(`meta[${attr}="${key}"]`);
if (!el) {
el = document.createElement("meta");
el.setAttribute(attr, key);
document.head.appendChild(el);
}
el.setAttribute("content", content);
};
// Standard meta
setMeta("name", "description", description);
if (keywords && keywords.length > 0) setMeta("name", "keywords", keywords.join(", "));
// OpenGraph
setMeta("property", "og:title", fullTitle);
setMeta("property", "og:description", description);
setMeta("property", "og:url", canonicalUrl);
setMeta("property", "og:image", ogImage);
setMeta("property", "og:type", ogType);
setMeta("property", "og:site_name", "TailTrails");
// Twitter Card
setMeta("name", "twitter:card", "summary_large_image");
setMeta("name", "twitter:title", fullTitle);
setMeta("name", "twitter:description", description);
setMeta("name", "twitter:image", ogImage);
// Canonical
let link = document.querySelector('link[rel="canonical"]') as HTMLLinkElement | null;
if (!link) {
link = document.createElement("link");
link.rel = "canonical";
document.head.appendChild(link);
}
link.href = canonicalUrl;
// hreflang (EN content; app also supports PT)
document.querySelectorAll('link[data-seo-hreflang]').forEach((l) => l.remove());
[
{ rel: "alternate", hreflang: "en", href: canonicalUrl },
{ rel: "alternate", hreflang: "x-default", href: canonicalUrl },
].forEach(({ rel, hreflang, href }) => {
const l = document.createElement("link");
l.rel = rel;
l.setAttribute("hreflang", hreflang);
l.href = href;
l.setAttribute("data-seo-hreflang", "true");
document.head.appendChild(l);
});
// Robots
if (noIndex) setMeta("name", "robots", "noindex, nofollow");
else if (noIndexFollow) setMeta("name", "robots", "noindex, follow");
else setMeta("name", "robots", "index, follow");
// JSON-LD
document.querySelectorAll('script[data-seo-jsonld]').forEach((s) => s.remove());
if (jsonLd) {
const schemas = Array.isArray(jsonLd) ? jsonLd : [jsonLd];
schemas.forEach((schema) => {
const script = document.createElement("script");
script.type = "application/ld+json";
script.setAttribute("data-seo-jsonld", "true");
script.textContent = JSON.stringify(schema);
document.head.appendChild(script);
});
}
}, [fullTitle, description, canonicalUrl, ogImage, ogType, jsonLd, noIndex, noIndexFollow, keywords]);
return null;
}
// ── Pre-built / factory JSON-LD schemas ──────────────────────────────────────
export const OrganizationSchema = {
"@context": "https://schema.org",
"@type": "Organization",
name: "TailTrails",
url: SITE_URL,
logo: `${SITE_URL}/tailtrails-logo.png`,
description:
"TailTrails is a mobile app that helps dog owners discover trails matched to their dog's needs and turn everyday walks into adventures. Available on iOS and Android.",
sameAs: [
"https://www.tiktok.com/@packlifeinportugal",
"https://www.facebook.com/packlifeinportugal",
"https://www.instagram.com/packlifeinportugal",
],
};
export const WebSiteSchema = {
"@context": "https://schema.org",
"@type": "WebSite",
name: "TailTrails",
url: SITE_URL,
};
export const MobileAppSchema = {
"@context": "https://schema.org",
"@type": "MobileApplication",
name: "TailTrails — Life Unleashed",
operatingSystem: "iOS, Android",
applicationCategory: "LifestyleApplication",
url: SITE_URL,
downloadUrl: [APP_STORE_URL, PLAY_STORE_URL],
offers: { "@type": "Offer", price: "0", priceCurrency: "EUR" },
description:
"Discover new trails that match your dog's needs, record your walks, and turn everyday walks into real adventures.",
};
export function makeBreadcrumbSchema(items: { name: string; url: string }[]) {
return {
"@context": "https://schema.org",
"@type": "BreadcrumbList",
itemListElement: items.map((item, i) => ({
"@type": "ListItem",
position: i + 1,
name: item.name,
item: item.url,
})),
};
}
export function makeArticleSchema(post: {
title: string;
description: string;
date: string;
slug: string;
category?: string;
}) {
return {
"@context": "https://schema.org",
"@type": "Article",
headline: post.title,
description: post.description,
datePublished: post.date,
dateModified: post.date,
author: { "@type": "Organization", name: "TailTrails" },
publisher: {
"@type": "Organization",
name: "TailTrails",
logo: { "@type": "ImageObject", url: `${SITE_URL}/tailtrails-logo.png` },
},
mainEntityOfPage: {
"@type": "WebPage",
"@id": `${SITE_URL}/blog/${post.slug}`,
},
articleSection: post.category || "Dog Walking",
};
}
// Comparison-page schema: breadcrumb + a Service listing for rich results.
export function makeComparisonSchema(competitorName: string, competitorUrl: string, slug: string) {
return [
makeBreadcrumbSchema([
{ name: "Home", url: `${SITE_URL}/` },
{ name: "Compare", url: `${SITE_URL}/compare` },
{ name: `TailTrails vs ${competitorName}`, url: `${SITE_URL}/compare/${slug}` },
]),
{
"@context": "https://schema.org",
"@type": "Service",
name: `TailTrails vs ${competitorName}`,
serviceType: "Dog walking & trail discovery app",
provider: { "@type": "Organization", name: "TailTrails", url: SITE_URL },
description: `Compare TailTrails vs ${competitorName}: dog walking apps and trail discovery tools for dog owners.`,
url: `${SITE_URL}/compare/${slug}`,
areaServed: "Worldwide",
about: [
{ "@type": "SoftwareApplication", name: "TailTrails", applicationCategory: "LifestyleApplication", operatingSystem: "iOS, Android", offers: { "@type": "Offer", price: "0", priceCurrency: "EUR" } },
{ "@type": "SoftwareApplication", name: competitorName, url: competitorUrl },
],
},
];
}
export function makeFAQSchema(faqs: { question: string; answer: string }[]) {
return {
"@context": "https://schema.org",
"@type": "FAQPage",
mainEntity: faqs.map((f) => ({
"@type": "Question",
name: f.question,
acceptedAnswer: { "@type": "Answer", text: f.answer },
})),
};
}
+8 -47
View File
@@ -1,4 +1,5 @@
import { motion } from 'motion/react';
import StoreButtons from './StoreButtons';
const CTA = () => {
return (
@@ -7,7 +8,7 @@ const CTA = () => {
<div className="glass rounded-[48px] p-8 md:p-20 text-center relative overflow-hidden border border-border-subtle">
<div className="absolute top-0 left-0 w-full h-full bg-brand/5 -z-10" />
<div className="absolute -top-24 -right-24 w-64 h-64 bg-brand/10 blur-[80px] rounded-full" />
<motion.div
initial={{ opacity: 0, scale: 0.95 }}
whileInView={{ opacity: 1, scale: 1 }}
@@ -19,54 +20,14 @@ const CTA = () => {
<span className="text-brand">Walking Journey?</span>
</h2>
<p className="text-text-secondary text-lg md:text-xl max-w-2xl mx-auto mb-10">
Join thousands of dog owners who are making every walk count. Download the app today!
Join thousands of dog owners who are making every walk count. TailTrails is completely free — download it today!
</p>
{/* Download Buttons Container */}
<div className="flex flex-col sm:flex-row items-center justify-center gap-4 max-w-md mx-auto">
{/* App Store Active Link */}
<motion.a
href="https://apps.apple.com/us/app/tail-trails-club/id6758676111"
target="_blank"
rel="noopener noreferrer"
whileHover={{ scale: 1.03, y: -2 }}
whileTap={{ scale: 0.98 }}
className="w-full sm:w-auto min-w-[200px] bg-brand text-zinc-950 px-6 py-4 rounded-2xl font-bold flex items-center justify-center gap-3 transition-colors shadow-lg shadow-brand/10"
>
{/* Apple SVG Icon */}
<svg className="w-6 h-6 fill-current" viewBox="0 0 24 24">
<path d="M18.71 19.5c-.83 1.24-1.71 2.45-3.05 2.47-1.34.03-1.77-.79-3.29-.79-1.53 0-2 .77-3.27.82-1.31.05-2.3-1.32-3.14-2.53C4.25 17 2.94 12.45 4.7 9.39c.87-1.52 2.43-2.48 4.12-2.51 1.28-.02 2.5.87 3.29.87.78 0 2.26-1.07 3.81-.91.65.03 2.47.26 3.64 1.98-.09.06-2.17 1.28-2.15 3.81.03 3.02 2.65 4.03 2.68 4.04-.03.07-.42 1.44-1.38 2.83M15.97 4.17c.66-.81 1.11-1.93.99-3.06-1 .04-2.21.67-2.93 1.49-.62.69-1.16 1.84-1.01 2.96 1.12.09 2.27-.58 2.95-1.39z"/>
</svg>
<div className="text-left leading-tight">
<p className="text-[11px] uppercase opacity-80 font-medium">Download on the</p>
<p className="text-base font-extrabold -mt-0.5">App Store</p>
</div>
</motion.a>
{/* Play Store Disabled / Coming Soon Button */}
<div
className="w-full sm:w-auto min-w-[200px] bg-zinc-900 border border-zinc-800 text-zinc-400 px-6 py-4 rounded-2xl font-bold flex items-center justify-center gap-3 relative group"
title="Android version coming soon!"
>
{/* Play Store SVG Icon */}
<svg className="w-6 h-6 fill-current opacity-50" viewBox="0 0 24 24">
<path d="M5,3.14V20.86c0,0.5,0.4,0.92,0.91,0.85l9.92-9.92L5.91,2.29C5.4,2.22,5,2.64,5,3.14Z"/>
<path d="M17.23,10.37,6.83,4.41a0.39,0.39,0,0,0-.41,0L15.8,13.8Z"/>
<path d="M15.8,13.8l-9.38,9.38a0.39,0.39,0,0,0,.41,0l10.4-5.96A2.16,2.16,0,0,0,17.23,13.63,2.18,2.18,0,0,0,17.23,10.37Z"/>
</svg>
<div className="text-left leading-tight">
<p className="text-[11px] uppercase opacity-60 font-medium">Get it on</p>
<p className="text-base font-extrabold -mt-0.5 whitespace-nowrap">Google Play</p>
</div>
{/* Absolute overlay badge indicating "Coming Soon" */}
<span className="absolute -top-2.5 right-3 bg-zinc-800 text-zinc-400 text-[10px] uppercase font-black px-2 py-0.5 rounded-full tracking-wider border border-zinc-700">
Soon
</span>
</div>
</div>
<StoreButtons className="max-w-md mx-auto" />
<p className="text-sm text-text-muted font-medium mt-5 flex items-center justify-center gap-2">
<span className="inline-flex items-center px-2.5 py-1 rounded-full bg-brand/15 text-brand text-xs font-bold uppercase tracking-wider">Free</span>
100% free — no subscription, no ads, no catch.
</p>
</motion.div>
</div>
</div>
+1 -1
View File
@@ -28,7 +28,7 @@ const Features = () => {
iconColor: "text-brand"
},
{
title: "Smart Marching",
title: "Smart Matching",
desc: "You tell us about your dog. Our community tags every trail. Then we combine this information - so you can choose where to go with confidence.",
icon: Tag,
size: "md:col-span-8",
+3 -3
View File
@@ -22,9 +22,9 @@ const Footer = () => {
<div>
<h4 className="font-bold mb-6 uppercase tracking-widest text-xs text-text-muted">Navigation</h4>
<ul className="space-y-4 font-medium">
<li><a href="#features" className="text-text-secondary hover:text-brand transition-colors">Features</a></li>
<li><a href="#how-it-works" className="text-text-secondary hover:text-brand transition-colors">How It Works</a></li>
<li><a href="/waitlist" className="text-text-secondary hover:text-brand transition-colors">Join Waitlist</a></li>
<li><a href="/#features" className="text-text-secondary hover:text-brand transition-colors">Features</a></li>
<li><a href="/compare" className="text-text-secondary hover:text-brand transition-colors">Compare</a></li>
<li><a href="/download" className="text-text-secondary hover:text-brand transition-colors">Get the App</a></li>
<li><a href="/privacy" className="text-text-secondary hover:text-brand transition-colors">Privacy Policy</a></li>
<li><a href="/terms" className="text-text-secondary hover:text-brand transition-colors">Terms &amp; Conditions</a></li>
</ul>
+9 -8
View File
@@ -24,11 +24,11 @@ const Header = () => {
</a>
<div className="hidden md:flex items-center gap-8">
<a href="#features" className="text-sm font-medium text-text-muted hover:text-brand transition-colors">Features</a>
<a href="#how-it-works" className="text-sm font-medium text-text-muted hover:text-brand transition-colors">How It Works</a>
<a href="/#features" className="text-sm font-medium text-text-muted hover:text-brand transition-colors">Features</a>
<a href="/blog" className="text-sm font-medium text-text-muted" onClick={() => setIsMenuOpen(false)}>Blogs</a>
<a href="/waitlist" className="bg-brand hover:bg-brand-light text-bg-primary px-6 py-2 rounded-full text-sm font-bold transition-all transform hover:scale-105 active:scale-95 inline-flex items-center justify-center">
Join Waitlist
<a href="/compare" className="text-sm font-medium text-text-muted hover:text-brand transition-colors">Compare</a>
<a href="/download" className="bg-brand hover:bg-brand-light text-bg-primary px-6 py-2 rounded-full text-sm font-bold transition-all transform hover:scale-105 active:scale-95 inline-flex items-center justify-center">
Get the App
</a>
</div>
@@ -47,10 +47,11 @@ const Header = () => {
className="absolute top-full left-6 right-6 mt-4 glass rounded-3xl p-6 md:hidden"
>
<div className="flex flex-col gap-4">
<a href="#features" className="text-lg font-medium text-text-muted" onClick={() => setIsMenuOpen(false)}>Features</a>
<a href="#how-it-works" className="text-lg font-medium text-text-muted" onClick={() => setIsMenuOpen(false)}>How It Works</a>
<a href="/waitlist" className="bg-brand text-bg-primary w-full py-4 rounded-2xl font-bold inline-flex items-center justify-center" onClick={() => setIsMenuOpen(false)}>
Join Waitlist
<a href="/#features" className="text-lg font-medium text-text-muted" onClick={() => setIsMenuOpen(false)}>Features</a>
<a href="/blog" className="text-lg font-medium text-text-muted" onClick={() => setIsMenuOpen(false)}>Blogs</a>
<a href="/compare" className="text-lg font-medium text-text-muted" onClick={() => setIsMenuOpen(false)}>Compare</a>
<a href="/download" className="bg-brand text-bg-primary w-full py-4 rounded-2xl font-bold inline-flex items-center justify-center" onClick={() => setIsMenuOpen(false)}>
Get the App
</a>
</div>
</motion.div>
+7 -5
View File
@@ -1,6 +1,6 @@
import { motion } from 'motion/react';
import { Zap, Facebook, Instagram } from 'lucide-react';
import PhoneMockup from './PhoneMockup';
import StoreButtons from './StoreButtons';
import { siFacebook, siInstagram, siTiktok } from 'simple-icons';
const Hero = () => {
@@ -28,10 +28,12 @@ const Hero = () => {
Discover new trails that match your dog's needs, and turn everyday walks into real adventures.
</p>
<div className="flex flex-col sm:flex-row items-center gap-6 mb-12">
<a href="/waitlist" className="w-full sm:w-auto bg-brand hover:bg-brand-light text-bg-primary px-10 py-5 rounded-2xl font-extrabold text-lg transition-all transform hover:scale-105 active:scale-95 flex items-center justify-center gap-2">
Join the Waitlist
</a>
<div className="mb-12">
<StoreButtons stack className="max-w-md" />
<p className="text-sm text-text-muted font-medium mt-5 flex items-center justify-center gap-2 sm:justify-start">
<span className="inline-flex items-center px-2.5 py-1 rounded-full bg-brand/15 text-brand text-xs font-bold uppercase tracking-wider">Free</span>
100% free — no subscription, no ads, no catch.
</p>
</div>
<div className="hidden sm:flex items-center gap-6">
+5 -4
View File
@@ -5,10 +5,10 @@ import { Facebook, Instagram, ShieldCheck, Users } from 'lucide-react';
const PhoneMockup = () => {
const [currentIndex, setCurrentIndex] = useState(0);
const images = [
"IMG_2721.PNG",
"IMG_2722.PNG",
"IMG_2724.PNG",
"IMG_2725.PNG"
"IMG_2721.webp",
"IMG_2722.webp",
"IMG_2724.webp",
"IMG_2725.webp"
];
useEffect(() => {
@@ -34,6 +34,7 @@ const PhoneMockup = () => {
transition={{ duration: 0.5 }}
className="w-full h-full object-cover"
referrerPolicy="no-referrer"
decoding="async"
/>
</AnimatePresence>
</div>
@@ -66,6 +66,8 @@ const ProblemSolution = () => {
src="/landing/sad-dog.avif"
alt="Problem with dog-friendly places"
className="w-full h-80 object-cover"
loading="lazy"
decoding="async"
/>
</div>
@@ -126,6 +128,8 @@ const ProblemSolution = () => {
src="/landing/happy-dog.avif"
alt="TailTrails solution"
className="w-full h-80 object-cover" // Synced height with Problem image for visual symmetry
loading="lazy"
decoding="async"
/>
</div>
+51
View File
@@ -0,0 +1,51 @@
import { motion } from 'motion/react';
import { APP_STORE_URL, PLAY_STORE_URL } from '@/lib/storeLinks';
type Props = {
/** Make buttons full width on mobile (used in hero stack). */
stack?: boolean;
className?: string;
};
// Reusable App Store + Google Play buttons. Active on both platforms.
const StoreButtons = ({ stack = false, className = '' }: Props) => (
<div className={`flex ${stack ? 'flex-col sm:flex-row' : 'flex-row'} items-center justify-center gap-4 ${className}`}>
<motion.a
href={APP_STORE_URL}
target="_blank"
rel="noopener noreferrer"
whileHover={{ scale: 1.03, y: -2 }}
whileTap={{ scale: 0.98 }}
className="w-full sm:w-auto min-w-[200px] bg-brand text-zinc-950 px-6 py-4 rounded-2xl font-bold flex items-center justify-center gap-3 transition-colors shadow-lg shadow-brand/10"
>
<svg className="w-6 h-6 fill-current" viewBox="0 0 24 24" aria-hidden="true">
<path d="M18.71 19.5c-.83 1.24-1.71 2.45-3.05 2.47-1.34.03-1.77-.79-3.29-.79-1.53 0-2 .77-3.27.82-1.31.05-2.3-1.32-3.14-2.53C4.25 17 2.94 12.45 4.7 9.39c.87-1.52 2.43-2.48 4.12-2.51 1.28-.02 2.5.87 3.29.87.78 0 2.26-1.07 3.81-.91.65.03 2.47.26 3.64 1.98-.09.06-2.17 1.28-2.15 3.81.03 3.02 2.65 4.03 2.68 4.04-.03.07-.42 1.44-1.38 2.83M15.97 4.17c.66-.81 1.11-1.93.99-3.06-1 .04-2.21.67-2.93 1.49-.62.69-1.16 1.84-1.01 2.96 1.12.09 2.27-.58 2.95-1.39z" />
</svg>
<div className="text-left leading-tight">
<p className="text-[11px] uppercase opacity-80 font-medium">Download on the</p>
<p className="text-base font-extrabold -mt-0.5">App Store</p>
</div>
</motion.a>
<motion.a
href={PLAY_STORE_URL}
target="_blank"
rel="noopener noreferrer"
whileHover={{ scale: 1.03, y: -2 }}
whileTap={{ scale: 0.98 }}
className="w-full sm:w-auto min-w-[200px] bg-brand text-zinc-950 px-6 py-4 rounded-2xl font-bold flex items-center justify-center gap-3 transition-colors shadow-lg shadow-brand/10"
>
<svg className="w-6 h-6 fill-current" viewBox="0 0 24 24" aria-hidden="true">
<path d="M5,3.14V20.86c0,0.5,0.4,0.92,0.91,0.85l9.92-9.92L5.91,2.29C5.4,2.22,5,2.64,5,3.14Z" />
<path d="M17.23,10.37,6.83,4.41a0.39,0.39,0,0,0-.41,0L15.8,13.8Z" />
<path d="M15.8,13.8l-9.38,9.38a0.39,0.39,0,0,0,.41,0l10.4-5.96A2.16,2.16,0,0,0,17.23,13.63,2.18,2.18,0,0,0,17.23,10.37Z" />
</svg>
<div className="text-left leading-tight">
<p className="text-[11px] uppercase opacity-80 font-medium">Get it on</p>
<p className="text-base font-extrabold -mt-0.5">Google Play</p>
</div>
</motion.a>
</div>
);
export default StoreButtons;
-14
View File
@@ -1,14 +0,0 @@
// Generated list of filenames in the `public/blog-images` folder.
// If you add or remove images, update this list accordingly.
const BLOG_IMAGE_FILENAMES = [
'afra-ramio-cp2HCVzguw4-unsplash.jpg',
'dagmar-klauzova-afBc-k2luH4-unsplash.jpg',
'eric-ward-ISg37AI2A-s-unsplash.jpg',
'jamie-street-m4BkRRprvP0-unsplash.jpg',
'joe-caione-qO-PIF84Vxg-unsplash.jpg',
'rafaella-waasdorp-EkzMdwI_YE4-unsplash.jpg',
'tatum-bergen-Vt3odrTM4cQ-unsplash.jpg',
'troy-bridges-lbRNtUZPXXQ-unsplash.jpg',
];
export default BLOG_IMAGE_FILENAMES;
-77
View File
@@ -1,77 +0,0 @@
export type BlogMeta = {
title?: string;
date?: string;
description?: string;
category?: string;
readTime?: string;
live?: boolean;
};
const meta: Record<string, BlogMeta> = {
"best-off-leash-areas-for-dogs-in-lagos-and-nearby": {
title: "Best Off-Leash Areas for Dogs in Lagos (and Nearby)",
date: "2024-05-01",
description: "Looking for off-leash areas for dogs in Lagos, Portugal? Discover the best spots for safe, enjoyable off-leash walks, plus what to watch out for.",
category: "Insights",
readTime: "6 min",
live: true,
},
"how-to-choose-the-right-walk-for-your-dog-based-on-their-personality-and-needs": {
title: "How to Choose the Right Walk for Your Dog (Based on Their Personality and Needs)",
date: "2024-04-20",
description: "Not all walks are equal. Learn how to choose the right environment for your dog based on energy, temperament, and needs.",
category: "Strategy",
readTime: "5 min",
live: true,
},
"how-to-give-a-reactive-dog-a-fulfilling-life-without-avoiding-the-world": {
title: "How to Give a Reactive Dog a Fulfilling Life (Without Avoiding the World)",
date: "2024-03-15",
description: "Have a reactive dog? Learn how to give them a fulfilling life with the right environments, structure, and approach.",
category: "Insights",
readTime: "7 min",
live: true,
},
"off-leash-dog-rules-in-portugal-what-you-need-to-know-in-2026": {
title: "Off-Leash Dog Rules in Portugal: What You Need to Know in 2026",
date: "2026-04-01",
description: "Can you walk your dog off-leash in Portugal? Learn the rules, risks, and best practices for safe and legal off-leash walks.",
category: "Trends",
readTime: "6 min",
live: true,
},
"top-10-hikes-to-do-with-your-dog-in-lagos-and-the-algarve": {
title: "Top 10 Hikes to do with your Dog in Lagos and the Algarve",
date: "2024-06-10",
description: "Discover the best hikes in Lagos and the Algarve to take your dog on. From coastal trails to quiet forests, find the right walk for your dog.",
category: "Insights",
readTime: "8 min",
live: true,
},
"walking-your-dog-in-the-algarve-hidden-dangers-every-owner-should-know": {
title: "Walking Your Dog in the Algarve: Hidden Dangers Every Owner Should Know",
date: "2024-07-05",
description: "Walking your dog in the Algarve? Learn about hidden dangers like pine processionary caterpillars, grass seeds, heat, and more — plus how to keep your dog safe.",
category: "Tutorial",
readTime: "7 min",
live: true,
},
"the-importance-of-variety-in-your-dogs-life-and-how-to-do-it-right": {
title: "The Importance of Variety in Your Dog’s Life (And How to Do It Right)",
date: "2024-02-28",
description: "Dogs need more than the same daily walk. Learn why variety matters and how to safely introduce new environments into your dog’s routine.",
category: "Strategy",
readTime: "5 min",
live: true,
},
"why-dog-friendly-isn-t-enough-and-what-actually-matters": {
title: "Why “Dog-Friendly” Isn’t Enough (And What Actually Matters)",
date: "2024-01-18",
description: "“Dog-friendly” doesn’t always mean suitable. Learn what really makes a walk right for your dog, and how to choose better environments.",
category: "Insights",
readTime: "5 min",
live: true,
},
};
export default meta;
-94
View File
@@ -1,94 +0,0 @@
export type BlogPost = {
slug: string;
title: string;
date?: string; // created at / published date
description?: string;
content: string;
category?: string;
readTime?: string;
live?: boolean;
};
import meta from './blogMeta';
function slugify(name: string) {
return name
.toLowerCase()
.replace(/\.md$/, '')
.replace(/[^a-z0-9]+/g, '-')
.replace(/(^-|-$)/g, '');
}
function parseFrontmatter(raw: string) {
const fmMatch = raw.match(/^---\n([\s\S]*?)\n---/);
const fm: Record<string, any> = {};
let content = raw;
if (fmMatch) {
const body = fmMatch[1];
body.split(/\n+/).forEach((line) => {
const idx = line.indexOf(':');
if (idx > -1) {
const key = line.slice(0, idx).trim();
let val = line.slice(idx + 1).trim().replace(/^"|"$/g, '');
// coerce booleans
if (/^(true|false)$/i.test(val)) {
fm[key] = val.toLowerCase() === 'true';
} else {
fm[key] = val;
}
}
});
content = raw.slice(fmMatch[0].length).trim();
}
return { fm, content };
}
let _postsCache: BlogPost[] | null = null;
async function loadPosts(): Promise<BlogPost[]> {
if (_postsCache) return _postsCache;
const slugs = Object.keys(meta);
const results: BlogPost[] = [];
for (const slug of slugs) {
try {
const url = `/raw-blogs/${slug}.md`;
const res = await fetch(url);
const raw = await res.text();
const { fm, content } = parseFrontmatter(raw);
const merged = { ...(meta[slug] || {}), ...fm } as Record<string, any>;
results.push({
slug,
title: merged.title ?? slug,
date: merged.date,
description: merged.description,
content,
category: merged.category,
readTime: merged.readTime,
live: typeof merged.live === 'boolean' ? merged.live : merged.live === 'true',
} as BlogPost);
} catch (e) {
// eslint-disable-next-line no-console
console.warn('Failed to load blog for slug:', slug, e);
}
}
results.sort((a, b) => {
const da = a.date ?? '';
const db = b.date ?? '';
return db.localeCompare(da);
});
_postsCache = results;
return results;
}
export async function getAllPosts(): Promise<BlogPost[]> {
return await loadPosts();
}
export async function getPostBySlug(slug: string): Promise<BlogPost | undefined> {
const all = await loadPosts();
return all.find((p) => p.slug === slug);
}
+6
View File
@@ -0,0 +1,6 @@
// Canonical store URLs — used everywhere on the site.
// Apple's country-less link auto-redirects to the visitor's regional store.
export const APP_STORE_URL =
"https://apps.apple.com/app/tailtrails-life-unleashed/id6758676111";
export const PLAY_STORE_URL =
"https://play.google.com/store/apps/details?id=com.tailtrailsclub.app";
+1 -33
View File
@@ -1,38 +1,6 @@
import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";
import BLOG_IMAGE_FILENAMES from './blogImages';
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
/**
* Return an Unsplash Source URL for a given query and size.
* Uses the Unsplash Source endpoint so no API key is required.
*/
export function getUnsplashImage(query: string, width = 1200, height = 675, seed?: string) {
const q = encodeURIComponent(query || 'nature');
// `sig` helps vary the image per post so images don't all cache to the same photo
const sig = seed ? `&sig=${encodeURIComponent(seed)}` : '';
return `https://source.unsplash.com/${width}x${height}/?${q}${sig}`;
}
// Load local blog images (as URLs) from the public folder root.
const _blogImageModules = import.meta.glob('/blog-images/*', { eager: true, query: '?url', import: 'default' }) as Record<string, string>;
let _blogImageUrls = Object.values(_blogImageModules);
// Fallback: if the glob didn't return anything (public assets may not be importable),
// construct URLs from known filenames in the `public/blog-images` folder.
if (!_blogImageUrls || _blogImageUrls.length === 0) {
_blogImageUrls = BLOG_IMAGE_FILENAMES.map((n) => `/blog-images/${n}`);
}
/**
* Pick a local blog image by numeric index. Images are assigned in order.
*/
export function getBlogImage(index?: number, fallback?: string) {
if (_blogImageUrls.length === 0) return fallback ?? '';
if (typeof index !== 'number' || isNaN(index)) return _blogImageUrls[0];
const idx = Math.abs(Math.floor(index)) % _blogImageUrls.length;
return _blogImageUrls[idx];
}
}
+42 -59
View File
@@ -1,10 +1,10 @@
import { useEffect, useState } from 'react';
import { useState } from 'react';
import { Link, useSearchParams } from 'react-router-dom';
import Header from '../components/landing/Header';
import Footer from '../components/landing/Footer';
import { Calendar, Clock, ArrowRight, Tag } from 'lucide-react';
import { getAllPosts, type BlogPost } from '../lib/blogs';
import { getBlogImage } from '../lib/utils';
import { getAllPosts, type BlogEntry } from '../blogs';
import SEO, { OrganizationSchema, WebSiteSchema, makeBreadcrumbSchema } from '../components/SEO';
import './blog-list.css';
const categoryColors: Record<string, string> = {
@@ -17,55 +17,38 @@ const categoryColors: Record<string, string> = {
const BlogList = () => {
const [searchParams] = useSearchParams();
const [posts, setPosts] = useState<BlogPost[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [selectedCategory, setSelectedCategory] = useState<string>('All');
const previewValue = (searchParams.get('preview') || '').toLowerCase();
const isPreview = previewValue === '1' || previewValue === 'true' || previewValue === 'yes';
useEffect(() => {
let active = true;
setLoading(true);
setError(null);
const all = getAllPosts();
const posts: BlogEntry[] = isPreview ? all : all.filter((p) => p.meta.live !== false);
const load = async () => {
try {
const all = await getAllPosts();
if (!active) return;
const filtered = isPreview ? all : all.filter((p: any) => p.live !== false);
setPosts(filtered as BlogPost[]);
} catch (err: any) {
if (!active) return;
setError(err?.message || 'Failed to load posts.');
} finally {
if (active) setLoading(false);
}
};
void load();
return () => {
active = false;
};
}, [isPreview]);
const [selectedCategory, setSelectedCategory] = useState<string>('All');
const categories = ['All', ...Array.from(new Set(posts.map((p) => (p as any).category || 'General'))).filter(Boolean)];
const displayed = selectedCategory === 'All' ? posts : posts.filter((p) => ((p as any).category || 'General') === selectedCategory);
const categories = ['All', ...Array.from(new Set(posts.map((p) => p.meta.category || 'General'))).filter(Boolean)];
const displayed = selectedCategory === 'All' ? posts : posts.filter((p) => (p.meta.category || 'General') === selectedCategory);
const featuredDisplayed = displayed.length ? displayed[0] : undefined;
const latest = displayed.length > 1 ? displayed.slice(1) : [];
return (
<div className="min-h-screen bg-background">
<SEO
title="Dog Walking Tips, Trails & Guides"
description="Guides on choosing the right walks for your dog, off-leash rules in Portugal, hidden dangers, and the best hikes in the Algarve."
keywords={["dog walking blog", "dog trails portugal", "off leash rules portugal", "dog hikes algarve", "reactive dog advice"]}
ogType="blog"
jsonLd={[OrganizationSchema, WebSiteSchema, makeBreadcrumbSchema([
{ name: "Home", url: "https://tailtrails.club/" },
{ name: "Blog", url: "https://tailtrails.club/blog" },
])]}
/>
<Header />
<main className="pt-32 pb-20">
<section className="container mx-auto px-6 mb-12">
{!loading && !featuredDisplayed && (
{!featuredDisplayed && (
<div className="glass rounded-2xl border border-border/50 p-8 text-center text-muted-foreground">
{error || 'No articles available yet.'}
No articles available yet.
</div>
)}
@@ -75,32 +58,32 @@ const BlogList = () => {
<div className="grid md:grid-cols-2 gap-0">
<div className="aspect-video md:aspect-auto overflow-hidden">
<img
src={getBlogImage(0)}
alt={featuredDisplayed.title}
src={featuredDisplayed.meta.image}
alt={featuredDisplayed.meta.title}
className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-500"
loading="eager"
decoding="async"
/>
</div>
<div className="p-8 md:p-12 flex flex-col justify-center">
<div className="flex items-center gap-3 mb-4">
{/** category may be undefined in current setup */}
<span className={`px-3 py-1 rounded-full text-xs font-medium ${categoryColors[(featuredDisplayed as any).category] || 'bg-secondary text-secondary-foreground'}`}>
{(featuredDisplayed as any).category || 'General'}
<span className={`px-3 py-1 rounded-full text-xs font-medium ${categoryColors[featuredDisplayed.meta.category] || 'bg-secondary text-secondary-foreground'}`}>
{featuredDisplayed.meta.category || 'General'}
</span>
<span className="text-sm text-muted-foreground">Featured</span>
</div>
<h2 className="text-2xl md:text-3xl font-bold text-foreground mb-4 group-hover:text-primary transition-colors">
{featuredDisplayed.title}
{featuredDisplayed.meta.title}
</h2>
<p className="text-muted-foreground mb-6">{featuredDisplayed.description}</p>
<p className="text-muted-foreground mb-6">{featuredDisplayed.meta.description}</p>
<div className="flex items-center gap-4 text-sm text-muted-foreground mb-6">
<span className="flex items-center gap-1">
<Calendar className="w-4 h-4" />
{featuredDisplayed.date}
{featuredDisplayed.meta.date}
</span>
<span className="flex items-center gap-1">
<Clock className="w-4 h-4" />
{/* readTime not available; omit if missing */}
{(featuredDisplayed as any).readTime || ''}
{featuredDisplayed.meta.readTime || ''}
</span>
</div>
<span className="inline-flex items-center gap-2 text-primary font-semibold group-hover:gap-3 transition-all">
@@ -135,9 +118,7 @@ const BlogList = () => {
</div>
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-8">
{loading && <div className="col-span-full text-center text-muted-foreground">Loading articles...</div>}
{!loading && latest.length === 0 && !!featuredDisplayed && (
{latest.length === 0 && !!featuredDisplayed && (
<div className="col-span-full text-center text-muted-foreground">No additional articles yet.</div>
)}
@@ -146,27 +127,29 @@ const BlogList = () => {
<article className="glass rounded-xl overflow-hidden border border-border/50 hover:border-primary/30 transition-colors">
<div className="aspect-video overflow-hidden">
<img
src={getBlogImage(i + 1)}
alt={p.title}
src={p.meta.image}
alt={p.meta.title}
className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-500"
loading="lazy"
decoding="async"
/>
</div>
<div className="p-6">
<div className="flex items-center gap-3 mb-3">
<span className={`px-3 py-1 rounded-full text-xs font-medium ${categoryColors[(p as any).category] || 'bg-secondary text-secondary-foreground'}`}>
{(p as any).category || 'General'}
<span className={`px-3 py-1 rounded-full text-xs font-medium ${categoryColors[p.meta.category] || 'bg-secondary text-secondary-foreground'}`}>
{p.meta.category || 'General'}
</span>
</div>
<h3 className="text-lg font-bold text-foreground mb-3 group-hover:text-primary transition-colors line-clamp-2">{p.title}</h3>
<p className="text-sm text-muted-foreground mb-4 line-clamp-2">{p.description}</p>
<h3 className="text-lg font-bold text-foreground mb-3 group-hover:text-primary transition-colors line-clamp-2">{p.meta.title}</h3>
<p className="text-sm text-muted-foreground mb-4 line-clamp-2">{p.meta.description}</p>
<div className="flex items-center gap-4 text-xs text-muted-foreground">
<span className="flex items-center gap-1">
<Calendar className="w-3 h-3" />
{p.date}
{p.meta.date}
</span>
<span className="flex items-center gap-1">
<Clock className="w-3 h-3" />
{(p as any).readTime || ''}
{p.meta.readTime || ''}
</span>
</div>
</div>
@@ -182,4 +165,4 @@ const BlogList = () => {
);
};
export default BlogList;
export default BlogList;
+35 -36
View File
@@ -1,37 +1,16 @@
import { useEffect, useState } from 'react';
import { useParams, useSearchParams } from 'react-router-dom';
import Header from '../components/landing/Header';
import Footer from '../components/landing/Footer';
import { getPostBySlug, getAllPosts, type BlogPost } from '../lib/blogs';
import { marked } from 'marked';
import { getBlogImage } from '../lib/utils';
import { Calendar, Clock, Tag } from 'lucide-react';
import { getPostBySlug, getAllPosts } from '../blogs';
import SEO, { OrganizationSchema, makeArticleSchema, makeBreadcrumbSchema } from '../components/SEO';
import { Calendar, Clock } from 'lucide-react';
import './blog.css';
const BlogPost = () => {
const { slug } = useParams();
const [searchParams] = useSearchParams();
const [post, setPost] = useState<BlogPost | undefined>();
const [index, setIndex] = useState<number | undefined>(undefined);
useEffect(() => {
if (!slug) return;
let active = true;
const load = async () => {
const p = await getPostBySlug(slug);
if (!active) return;
setPost(p);
if (p) {
const posts = await getAllPosts();
const idx = posts.findIndex((x) => x.slug === p.slug);
setIndex(idx >= 0 ? idx : undefined);
}
};
void load();
return () => {
active = false;
};
}, [slug]);
const post = slug ? getPostBySlug(slug) : undefined;
const index = post ? getAllPosts().findIndex((x) => x.slug === post.slug) : undefined;
if (!post) {
return (
@@ -48,35 +27,55 @@ const BlogPost = () => {
}
const isPreview = ['1', 'true', 'yes'].includes((searchParams.get('preview') || '').toLowerCase());
const { meta, Component } = post;
return (
<div className="min-h-screen bg-background">
<SEO
title={meta.title}
description={meta.description}
ogType="article"
keywords={["dog walking", "dog trails", "dog walks portugal", meta.category]}
jsonLd={[
makeArticleSchema({ title: meta.title, description: meta.description, date: meta.date, slug: post.slug, category: meta.category }),
makeBreadcrumbSchema([
{ name: "Home", url: "https://tailtrails.club/" },
{ name: "Blog", url: "https://tailtrails.club/blog" },
{ name: meta.title, url: `https://tailtrails.club/blog/${post.slug}` },
]),
OrganizationSchema,
]}
/>
<Header />
<main className="pt-32 pb-20">
<section className="container mx-auto px-6">
<div className="glass rounded-2xl overflow-hidden border border-border/50">
<div className="w-full">
{index !== undefined && (
{index !== undefined && index >= 0 && (
<img
src={getBlogImage(index)}
alt={post.title}
src={meta.image}
alt={meta.title}
className="w-full h-64 md:h-96 object-cover group-hover:scale-105 transition-transform duration-500"
loading="eager"
decoding="async"
/>
)}
<div className="p-8 md:p-12">
<div className="flex items-center gap-3 mb-3">
<span className="px-3 py-1 rounded-full text-xs font-medium bg-secondary text-secondary-foreground">{(post as any).category || 'General'}</span>
<div className="text-sm text-muted-foreground">{post.date} {isPreview ? '(Preview)' : ''}</div>
<span className="px-3 py-1 rounded-full text-xs font-medium bg-secondary text-secondary-foreground">{meta.category || 'General'}</span>
<div className="text-sm text-muted-foreground">{meta.date} {isPreview ? '(Preview)' : ''}</div>
</div>
<h1 className="text-3xl md:text-4xl font-bold text-foreground mb-4">{post.title}</h1>
<h1 className="text-3xl md:text-4xl font-bold text-foreground mb-4">{meta.title}</h1>
<div className="flex items-center gap-6 text-sm text-muted-foreground mb-6">
<span className="flex items-center gap-2"><Calendar className="w-4 h-4" />{post.date}</span>
<span className="flex items-center gap-2"><Clock className="w-4 h-4" />{(post as any).readTime || ''}</span>
<span className="flex items-center gap-2"><Calendar className="w-4 h-4" />{meta.date}</span>
<span className="flex items-center gap-2"><Clock className="w-4 h-4" />{meta.readTime || ''}</span>
</div>
<article className="prose max-w-none text-foreground post-content" dangerouslySetInnerHTML={{ __html: marked.parse(post.content) }} />
<article className="prose max-w-none text-foreground post-content">
<Component />
</article>
</div>
</div>
</div>
@@ -88,4 +87,4 @@ const BlogPost = () => {
);
};
export default BlogPost;
export default BlogPost;
+139
View File
@@ -0,0 +1,139 @@
import { Link } from 'react-router-dom';
import Header from '@/components/landing/Header';
import Footer from '@/components/landing/Footer';
import SEO, { OrganizationSchema, WebSiteSchema, makeBreadcrumbSchema } from '@/components/SEO';
import { Scale, BarChart3, PawPrint, Map, Heart } from 'lucide-react';
const comparisons = [
{
slug: 'dog-walking-apps-compared',
title: 'Dog Walking & Trail Apps Compared: Which One Actually Fits Your Dog?',
excerpt:
'We compared the leading dog walking apps and trail discovery tools — free vs paid, hardware vs phone-only, generic dog-friendly filters vs trails matched to your dog. See the full table.',
tag: 'Industry Analysis',
icon: BarChart3,
image: '/blog-images/how-to-choose-the-right-walk-for-your-dog-based-on-their-personality-and-needs.webp',
},
{
slug: 'tailtrails-vs-wooftrax',
title: 'TailTrails vs WoofTrax (Walk for a Dog)',
excerpt:
'Both are free apps that record your dog walks. WoofTrax turns walks into shelter donations; TailTrails matches trails to your dog and helps you discover new ones. Which fits you?',
tag: 'Free Walk App',
icon: PawPrint,
},
{
slug: 'tailtrails-vs-tractive',
title: 'TailTrails vs Tractive',
excerpt:
'Tractive is a GPS collar for finding a lost dog. TailTrails is a free app for finding the right walk. They solve different problems — here is which you need (and why you might want both).',
tag: 'Hardware Tracker',
icon: Map,
},
{
slug: 'tailtrails-vs-alltrails',
title: 'TailTrails vs AllTrails ("AllTrails for Dogs")',
excerpt:
'AllTrails has a "dog-friendly" filter across a huge trail database. TailTrails matches trails to your dog’s energy, behaviour and needs. One is generic, one is personal.',
tag: 'Trail Discovery',
icon: Map,
},
{
slug: 'tailtrails-vs-fitbark',
title: 'TailTrails vs FitBark',
excerpt:
'FitBark is a GPS and health collar with a subscription. TailTrails is a free phone app for walk discovery and recording. Different goals, different costs.',
tag: 'Hardware Tracker',
icon: Heart,
},
{
slug: 'tailtrails-vs-pitpat',
title: 'TailTrails vs PitPat',
excerpt:
'PitPat is a UK activity and GPS device you wear on the collar. TailTrails is a free app with no hardware. Which suits your dog walking routine?',
tag: 'Hardware Tracker',
icon: PawPrint,
},
];
const Compare = () => {
const featured = comparisons[0];
const rest = comparisons.slice(1);
return (
<div className="min-h-screen bg-background">
<SEO
title="Dog Walking App Comparisons"
description="Honest comparisons of dog walking apps and trail discovery tools: TailTrails vs WoofTrax, Tractive, AllTrails, FitBark and PitPat. Find the right app for you and your dog."
keywords={["dog walking app comparison", "tailtrails vs wooftrax", "tailtrails vs tractive", "alltrails for dogs", "best dog walking app", "dog trail app"]}
jsonLd={[OrganizationSchema, WebSiteSchema, makeBreadcrumbSchema([
{ name: 'Home', url: 'https://tailtrails.club/' },
{ name: 'Compare', url: 'https://tailtrails.club/compare' },
])]}
/>
<Header />
<main className="pt-32 pb-20">
<section className="container mx-auto px-6 mb-12">
<div className="max-w-2xl mb-12">
<h1 className="text-3xl md:text-5xl font-display font-bold text-text-primary mb-4">Dog Walking App Comparisons</h1>
<p className="text-lg text-text-secondary">
Not every "dog" app does the same job. Some track your walk, some find your lost dog, some list dog-friendly places.
These comparisons help you pick the one that actually fits how you walk your dog.
</p>
</div>
<Link to={`/compare/${featured.slug}`} className="group block">
<div className="glass rounded-2xl overflow-hidden border border-border/50 hover:border-brand/30 transition-colors">
<div className="grid md:grid-cols-2 gap-0">
<div className="p-8 md:p-12 flex flex-col justify-center">
<div className="flex items-center gap-3 mb-4">
<span className="px-3 py-1 rounded-full text-xs font-medium bg-brand/15 text-brand">{featured.tag}</span>
<span className="text-sm text-text-muted">Featured</span>
</div>
<h2 className="text-2xl md:text-3xl font-bold text-text-primary mb-4 group-hover:text-brand transition-colors">{featured.title}</h2>
<p className="text-text-secondary mb-6">{featured.excerpt}</p>
<span className="inline-flex items-center gap-2 text-brand font-semibold group-hover:gap-3 transition-all">Read the comparison →</span>
</div>
<div className="hidden md:block overflow-hidden">
<img
src={featured.image}
alt={featured.title}
className="w-full h-full object-cover min-h-[320px]"
loading="eager"
decoding="async"
/>
</div>
</div>
</div>
</Link>
</section>
<section className="container mx-auto px-6">
<h2 className="text-2xl font-bold text-text-primary mb-8">All Comparisons</h2>
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-6">
{rest.map((c) => (
<Link key={c.slug} to={`/compare/${c.slug}`} className="group block">
<article className="glass rounded-xl overflow-hidden border border-border/50 hover:border-brand/30 transition-colors h-full">
<div className="p-6 flex flex-col h-full">
<div className="flex items-center gap-3 mb-3">
<c.icon className="w-5 h-5 text-brand" />
<span className="px-3 py-1 rounded-full text-xs font-medium bg-brand/15 text-brand">{c.tag}</span>
</div>
<h3 className="text-lg font-bold text-text-primary mb-3 group-hover:text-brand transition-colors">{c.title}</h3>
<p className="text-sm text-text-secondary mb-4 flex-1">{c.excerpt}</p>
<span className="text-brand font-semibold text-sm group-hover:gap-2 transition-all inline-flex items-center gap-1">Read →</span>
</div>
</article>
</Link>
))}
</div>
</section>
</main>
<Footer />
</div>
);
};
export default Compare;
+5 -1
View File
@@ -13,8 +13,12 @@ const Download: React.FC = () => {
window.location.replace(APP_STORE_URL);
} else if (/android/i.test(ua)) {
window.location.replace(PLAY_STORE_URL);
} else if (/Macintosh|MacIntel|MacPPC|Mac68K/.test(ua)) {
// macOS desktop — send to the App Store (iOS listing)
window.location.replace(APP_STORE_URL);
} else {
window.location.replace(FALLBACK_URL);
// Windows / Linux / other desktops — send to Google Play
window.location.replace(PLAY_STORE_URL);
}
}, []);
+2
View File
@@ -1,6 +1,7 @@
import * as React from "react";
import { useLocation } from "react-router-dom";
import { toast } from "@/components/ui/use-toast";
import SEO from "@/components/SEO";
const FinalizeAccountDeletion: React.FC = () => {
const location = useLocation();
@@ -44,6 +45,7 @@ const FinalizeAccountDeletion: React.FC = () => {
return (
<main className="min-h-screen bg-bg-primary">
<SEO title="Confirm Account Deletion" description="Confirm deletion of your TailTrails account." noIndex />
<section className="pt-20 pb-20">
<div className="container mx-auto max-w-4xl px-6 min-h-[60vh] flex items-center">
<div className="bento-card">
+8 -1
View File
@@ -5,10 +5,17 @@ import HowItWorks from "@/components/landing/HowItWorks";
import ProblemSolution from "@/components/landing/ProblemSolution";
import CTA from "@/components/landing/CTA";
import Footer from "@/components/landing/Footer";
import SEO, { OrganizationSchema, WebSiteSchema, MobileAppSchema } from "@/components/SEO";
const Index = () => {
return (
<main className="min-h-screen">
<SEO
title="Life Unleashed"
description="Discover new trails that match your dog's needs, record your walks, and turn everyday walks into real adventures. TailTrails is completely free on iOS and Android."
keywords={["dog walking app", "dog trails", "dog walk tracker", "find dog walks", "TailTrails"]}
jsonLd={[OrganizationSchema, WebSiteSchema, MobileAppSchema]}
/>
<Header />
<Hero />
<Features />
@@ -20,4 +27,4 @@ const Index = () => {
);
};
export default Index;
export default Index;
+2
View File
@@ -1,5 +1,6 @@
import { useLocation } from "react-router-dom";
import { useEffect } from "react";
import SEO from "@/components/SEO";
const NotFound = () => {
const location = useLocation();
@@ -10,6 +11,7 @@ const NotFound = () => {
return (
<div className="flex min-h-screen items-center justify-center bg-muted">
<SEO title="Page Not Found" description="This page could not be found." noIndex />
<div className="text-center">
<h1 className="mb-4 text-4xl font-bold">404</h1>
<p className="mb-4 text-xl text-muted-foreground">Oops! Page not found</p>
+2
View File
@@ -1,8 +1,10 @@
import React from "react";
import SEO from "@/components/SEO";
const Privacy: React.FC = () => {
return (
<div className="container mx-auto px-4 py-12">
<SEO title="Privacy Policy" description="How TailTrails collects, uses, and protects your information." />
<div className="prose max-w-none">
<h1>Privacy Policy</h1>
<p>Last updated: February 13, 2026</p>
+2
View File
@@ -1,5 +1,6 @@
import * as React from "react";
import { toast } from "@/components/ui/use-toast";
import SEO from "@/components/SEO";
const RequestAccountDeletion: React.FC = () => {
const [email, setEmail] = React.useState("");
@@ -40,6 +41,7 @@ const RequestAccountDeletion: React.FC = () => {
return (
<main className="min-h-screen bg-bg-primary">
<SEO title="Delete Your Account" description="Request deletion of your TailTrails account and associated data." noIndex />
<section className="pt-20 pb-20">
<div className="container mx-auto max-w-4xl px-6 min-h-[60vh] flex items-center">
<div className="bento-card">
+2
View File
@@ -1,8 +1,10 @@
import React from "react";
import SEO from "@/components/SEO";
const Terms: React.FC = () => {
return (
<div className="container mx-auto px-4 py-12">
<SEO title="Terms and Conditions" description="The terms and conditions for using the TailTrails app and website." />
<div className="prose max-w-none">
<h1>Terms and Conditions</h1>
<p>Last updated: August 20, 2026</p>
-108
View File
@@ -1,108 +0,0 @@
import * as React from "react";
import { toast } from "@/components/ui/use-toast";
const Waitlist: React.FC = () => {
const [name, setName] = React.useState("");
const [email, setEmail] = React.useState("");
const [loading, setLoading] = React.useState(false);
const onSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!name.trim() || !email.trim()) {
toast({ title: "Please provide name and email." });
return;
}
setLoading(true);
try {
const envBase = (import.meta as any).env?.VITE_API_URL as string | undefined;
const base = envBase ? envBase.replace(/\/$/, "") : "http://localhost:8080";
const url = `${base}/waitlist?name=${encodeURIComponent(name)}&email=${encodeURIComponent(email)}`;
const res = await fetch(url, { method: "POST" });
if (!res.ok) {
const text = await res.text().catch(() => "");
throw new Error(text || `Status ${res.status}`);
}
toast({ title: "Success", description: "You've been added to the waitlist." });
setName("");
setEmail("");
// redirect to home after 0.5 seconds
setTimeout(() => {
window.location.href = "/";
}, 500);
} catch (err: any) {
toast({ title: "Error", description: err?.message ?? String(err) });
} finally {
setLoading(false);
}
};
return (
<main className="min-h-screen bg-bg-primary">
<section className="pt-20 pb-20">
<div className="container mx-auto max-w-4xl px-6 min-h-[60vh] flex items-center">
<div className="bento-card">
<h3 className="text-center text-2xl md:text-3xl font-semibold text-text-primary mb-6">Be the first to know as soon as the TailTrails App is live</h3>
<div className="flex flex-col md:flex-row items-center gap-8">
<div className="flex-1 flex items-center justify-center md:justify-start">
<div className="flex items-center gap-6">
<div className="w-40 h-40 bg-accent-primary rounded-full flex items-center justify-center border border-border-subtle/40">
<img src="/tailtrails-logo.png" alt="TailTrails Logo" className="w-24 h-24" />
</div>
<div>
<div className="text-3xl font-bold text-text-primary">TailTrails</div>
<div className="text-sm text-text-muted">Find trails tailored to your dog</div>
</div>
</div>
</div>
<div className="w-full md:w-[350px]">
<div className="glass rounded-2xl p-6">
<form onSubmit={onSubmit} className="space-y-4">
<div>
<label className="sr-only">Name</label>
<input
className="w-full rounded-md border border-border-subtle/20 px-3 py-2 placeholder:text-text-muted glass text-text-primary"
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="Name"
required
/>
</div>
<div>
<label className="sr-only">Email</label>
<input
type="email"
className="w-full rounded-md border border-border-subtle/20 px-3 py-2 placeholder:text-text-muted glass text-text-primary"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="Email *"
required
/>
</div>
<div>
<button
type="submit"
className="w-full rounded-md bg-brand hover:bg-brand-light text-bg-primary font-semibold py-2 hover:opacity-95 disabled:opacity-60"
disabled={loading}
>
{loading ? "Sending..." : "Join our Pack"}
</button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
</section>
</main>
);
};
export default Waitlist;
@@ -0,0 +1,184 @@
import { Link } from 'react-router-dom';
import Header from '@/components/landing/Header';
import Footer from '@/components/landing/Footer';
import SEO, { OrganizationSchema, makeBreadcrumbSchema, makeFAQSchema } from '@/components/SEO';
import '../blog.css';
// Honest, defensible comparison. ✓ = yes, — = no, ~ = partial / paid.
const rows = [
{ name: 'TailTrails', type: 'Phone app', free: '✓ Free', hardware: '— None', record: '✓ GPS', discover: '✓ Matched to your dog', community: '✓ Shared trails', sub: '— None' },
{ name: 'WoofTrax', type: 'Phone app', free: '✓ Free (ads)', hardware: '— None', record: '✓ GPS', discover: '— No', community: '~ Groups', sub: '— None' },
{ name: 'Tractive', type: 'GPS collar', free: '~ Paid', hardware: '✓ Collar + sub', record: '~ Walk feature', discover: '— No', community: '~ Family sharing', sub: '✓ Monthly' },
{ name: 'PitPat', type: 'Activity / GPS device', free: '~ Paid', hardware: '✓ Device', record: '~ Activity', discover: '— No', community: '~ Family', sub: '~ Optional' },
{ name: 'FitBark', type: 'GPS / health collar', free: '~ Paid', hardware: '✓ Collar + sub', record: '~ Walk feature', discover: '— No', community: '~ Journal', sub: '✓ Monthly' },
{ name: 'AllTrails', type: 'Trail app', free: '~ Freemium', hardware: '— None', record: '✓ GPS', discover: '~ "Dog-friendly" filter', community: '✓ Reviews', sub: '✓ Optional' },
{ name: 'BringFido', type: 'Places directory', free: '✓ Free', hardware: '— None', record: '— No', discover: '~ Dog-friendly places', community: '✓ Reviews', sub: '— None' },
{ name: 'Sniffspot', type: 'Private rentals', free: '~ Pay per visit', hardware: '— None', record: '— No', discover: '~ Private spaces', community: '~ Host reviews', sub: '— None' },
];
// Mobile cards show the most decision-relevant fields.
const cardFields = [
{ key: 'free', label: 'Cost' },
{ key: 'hardware', label: 'Hardware' },
{ key: 'record', label: 'Records walks' },
{ key: 'discover', label: 'Trail discovery' },
{ key: 'sub', label: 'Subscription' },
] as const;
const faqs = [
{ q: 'What is the best free dog walking app?', a: 'It depends on your goal. WoofTrax is great if you want your walks to support a shelter. TailTrails is the stronger choice if you want to discover new trails that actually suit your dog, not just record the ones you already do. Both are free.' },
{ q: 'Is TailTrails a GPS pet tracker?', a: 'No. TailTrails records your walks using your phone’s GPS and helps you find trails matched to your dog. It does not track your dog’s live location when off the lead. For a lost-dog scenario you want a GPS collar like Tractive, PitPat or FitBark.' },
{ q: 'Can I use TailTrails instead of a GPS collar?', a: 'For walk discovery and recording, yes. For escape recovery, no — those are different problems. Many owners use a free walk app like TailTrails day to day and a GPS collar only for dogs that bolt.' },
{ q: 'Is AllTrails good for dogs?', a: 'AllTrails has a "dog-friendly" filter and a huge trail database, but it does not know anything about your specific dog. A trail being dog-friendly does not mean it suits a reactive, high-energy or older dog. TailTrails matches trails to your dog’s traits.' },
{ q: 'Do these apps require a subscription?', a: 'TailTrails and WoofTrax are free. AllTrails is free with paid tiers for offline maps. Tractive and FitBark require a subscription because they use cellular hardware. PitPat offers a no-subscription option.' },
];
const BigComparison = () => {
return (
<div className="min-h-screen bg-background">
<SEO
title="Dog Walking & Trail Apps Compared (2026)"
description="Side-by-side comparison of dog walking apps and trail tools: TailTrails, WoofTrax, Tractive, PitPat, FitBark, AllTrails, BringFido and Sniffspot. Free vs paid, hardware vs phone, generic vs matched to your dog."
keywords={["dog walking app comparison", "best dog walking app", "dog trail app", "alltrails for dogs", "free dog walk tracker"]}
jsonLd={[
OrganizationSchema,
makeBreadcrumbSchema([
{ name: 'Home', url: 'https://tailtrails.club/' },
{ name: 'Compare', url: 'https://tailtrails.club/compare' },
{ name: 'All Apps Compared', url: 'https://tailtrails.club/compare/dog-walking-apps-compared' },
]),
makeFAQSchema(faqs),
]}
/>
<Header />
<main className="pt-32 pb-20">
<section className="container mx-auto px-6">
{/* Wider than the default article column so the 8-column table breathes. */}
<div className="max-w-7xl mx-auto">
<h1 className="text-3xl md:text-4xl font-bold text-text-primary mb-6">Dog Walking & Trail Apps Compared</h1>
<p className="text-lg text-text-secondary mb-8 max-w-3xl">
The "dog app" category is confusing because the apps do very different things. Some record your walk.
Some find a lost dog. Some list dog-friendly places. This table puts them side by side so you can see
which problem each one actually solves — and which one fits how you walk your dog.
</p>
<article className="post-content max-w-none text-text-primary">
<h2>The full comparison</h2>
<p>Legend: <strong>✓</strong> yes &middot; <strong>~</strong> partial or paid &middot; <strong>—</strong> no.</p>
{/* Desktop / tablet: compact table, no horizontal scroll needed. */}
<div className="hidden md:block overflow-x-auto mb-8">
<table className="w-full text-xs border-collapse">
<thead>
<tr className="border-b border-border-subtle">
<th className="text-left p-3 font-bold text-text-primary whitespace-nowrap">App</th>
<th className="text-left p-3 font-bold text-text-primary whitespace-nowrap">Type</th>
<th className="text-left p-3 font-bold text-text-primary whitespace-nowrap">Cost</th>
<th className="text-left p-3 font-bold text-text-primary whitespace-nowrap">Hardware</th>
<th className="text-left p-3 font-bold text-text-primary whitespace-nowrap">Records walks</th>
<th className="text-left p-3 font-bold text-text-primary whitespace-nowrap">Trail discovery</th>
<th className="text-left p-3 font-bold text-text-primary whitespace-nowrap">Community</th>
<th className="text-left p-3 font-bold text-text-primary whitespace-nowrap">Subscription</th>
</tr>
</thead>
<tbody>
{rows.map((r) => (
<tr key={r.name} className={`border-b border-border-subtle/60 ${r.name === 'TailTrails' ? 'bg-brand/5' : ''}`}>
<td className="p-3 font-semibold text-text-primary whitespace-nowrap">{r.name}</td>
<td className="p-3 text-text-secondary whitespace-nowrap">{r.type}</td>
<td className="p-3 text-text-secondary whitespace-nowrap">{r.free}</td>
<td className="p-3 text-text-secondary whitespace-nowrap">{r.hardware}</td>
<td className="p-3 text-text-secondary whitespace-nowrap">{r.record}</td>
<td className="p-3 text-text-secondary whitespace-nowrap">{r.discover}</td>
<td className="p-3 text-text-secondary whitespace-nowrap">{r.community}</td>
<td className="p-3 text-text-secondary whitespace-nowrap">{r.sub}</td>
</tr>
))}
</tbody>
</table>
</div>
{/* Mobile: stacked cards instead of a cramped, scrolling table. */}
<div className="md:hidden space-y-3 mb-8">
{rows.map((r) => (
<div key={r.name} className={`glass rounded-xl border p-4 ${r.name === 'TailTrails' ? 'border-brand/40 bg-brand/5' : 'border-border/50'}`}>
<div className="flex items-center justify-between mb-3">
<span className="font-bold text-text-primary">{r.name}</span>
<span className="text-xs text-text-muted">{r.type}</span>
</div>
<dl className="grid grid-cols-2 gap-x-3 gap-y-2 text-sm">
{cardFields.map(({ key, label }) => (
<div key={key} className="flex flex-col">
<dt className="text-[11px] uppercase tracking-wide text-text-muted">{label}</dt>
<dd className="text-text-secondary">{(r as any)[key]}</dd>
</div>
))}
</dl>
</div>
))}
</div>
<h2>Three different problems — do not confuse them</h2>
<p>Most "dog app" searches actually fall into one of three needs:</p>
<ul>
<li><strong>Record my walks</strong> — a free phone app like TailTrails or WoofTrax. No hardware, no subscription.</li>
<li><strong>Find my lost dog</strong> — a GPS collar like Tractive, PitPat or FitBark. Hardware + (usually) a subscription.</li>
<li><strong>Find somewhere to go</strong> — a trail or places app like AllTrails, BringFido or Sniffspot.</li>
</ul>
<p>
TailTrails is the only app in this list that does the first <em>and</em> the third together — and crucially,
it matches trails to <strong>your dog</strong>, not just a generic "dog-friendly" label. A trail can be dog-friendly
and still be wrong for a reactive, high-energy or older dog. That match is the difference between a good walk
and a stressful one.
</p>
<h2>Why "dog-friendly" is not enough</h2>
<p>
A beach that allows dogs, a forest that allows dogs, a park that allows dogs — these are all "dog-friendly."
But a reactive dog needs space and low traffic, a high-energy dog needs open ground to run, and an older dog
needs flat, shaded terrain. Generic filters cannot tell you that. TailTrails uses community tags
(environment, terrain, traffic, exposure) combined with your dog’s profile to suggest walks that bring out
the best in your dog. Read more in our article on
{' '}<Link to="/blog/how-to-choose-the-right-walk-for-your-dog-based-on-their-personality-and-needs" className="text-brand">choosing the right walk for your dog</Link>.
</p>
<h2>Free vs paid: what you actually spend</h2>
<p>
TailTrails and WoofTrax are free. AllTrails is free to browse but pushes a paid tier for offline maps and
wrong-turn alerts. The hardware trackers (Tractive, FitBark) require a subscription — the device is unusable
without it — so the real cost is the device plus years of monthly fees. If your goal is better walks rather
than lost-dog recovery, a free app covers it.
</p>
<h2>The honest verdict</h2>
<p>
There is no single winner because the apps solve different problems. If you want to <strong>find new walks that
suit your dog</strong> and record them for free, TailTrails is built for that. If your dog is an escape risk,
add a GPS collar. If you just want to support a shelter with your daily walk, WoofTrax is lovely. Use the
table above to pick the one that matches your actual need.
</p>
</article>
<h2 className="text-2xl font-bold text-text-primary mt-12 mb-6">Read the individual comparisons</h2>
<ul className="space-y-3">
<li><Link to="/compare/tailtrails-vs-wooftrax" className="text-brand hover:underline">TailTrails vs WoofTrax →</Link></li>
<li><Link to="/compare/tailtrails-vs-tractive" className="text-brand hover:underline">TailTrails vs Tractive →</Link></li>
<li><Link to="/compare/tailtrails-vs-alltrails" className="text-brand hover:underline">TailTrails vs AllTrails →</Link></li>
<li><Link to="/compare/tailtrails-vs-fitbark" className="text-brand hover:underline">TailTrails vs FitBark →</Link></li>
<li><Link to="/compare/tailtrails-vs-pitpat" className="text-brand hover:underline">TailTrails vs PitPat →</Link></li>
</ul>
<div className="mt-10">
<Link to="/compare" className="text-brand font-semibold hover:underline">← Back to all comparisons</Link>
</div>
</div>
</section>
</main>
<Footer />
</div>
);
};
export default BigComparison;
@@ -0,0 +1,88 @@
import { Link } from 'react-router-dom';
import Header from '@/components/landing/Header';
import Footer from '@/components/landing/Footer';
import SEO, { makeComparisonSchema } from '@/components/SEO';
import '../blog.css';
const TailtrailsVsAlltrails = () => (
<div className="min-h-screen bg-background">
<SEO
title="TailTrails vs AllTrails (AllTrails for Dogs)"
description="AllTrails has a dog-friendly filter across a huge trail database; TailTrails matches trails to your dog’s energy, behaviour and needs. One is generic, one is personal. See the difference."
keywords={["alltrails for dogs", "tailtrails vs alltrails", "dog friendly trails app", "dog trail finder"]}
jsonLd={makeComparisonSchema('AllTrails', 'https://www.alltrails.com', 'tailtrails-vs-alltrails')}
/>
<Header />
<main className="pt-32 pb-20">
<section className="container mx-auto px-6">
<div className="max-w-3xl mx-auto">
<h1 className="text-3xl md:text-4xl font-bold text-text-primary mb-6">TailTrails vs AllTrails ("AllTrails for Dogs")</h1>
<p className="text-lg text-text-secondary mb-8">
"Is there an AllTrails for dogs?" is one of the most common questions from dog owners who hike. AllTrails is the
biggest trail app in the world and it does have a dog-friendly filter. TailTrails is dog-first and dog-only. The
difference sounds small until you actually use them — then it is the difference between "allowed" and "right."
</p>
<article className="post-content max-w-none text-text-primary">
<h2>What AllTrails does well</h2>
<p>
AllTrails has an enormous trail database, great search filters (distance, elevation, difficulty, trail type), photos,
reviews, and GPS recording. The "dogs allowed" filter narrows it to trails where dogs are permitted. For general
hiking it is excellent, and if you already use it, the dog filter is a reasonable way to shortlist routes.
</p>
<p>AllTrails is strong if you want:</p>
<ul>
<li>The widest possible trail coverage, worldwide</li>
<li>Hiking-focused detail — elevation, difficulty, conditions</li>
<li>A "dogs allowed" yes/no filter on top of that</li>
</ul>
<h2>The problem with "dog-friendly"</h2>
<p>
A trail being dog-friendly just means dogs are <em>allowed</em>. It says nothing about whether it suits <em>your</em>
dog. A narrow cliff path is dog-friendly but wrong for a reactive dog that needs space. A busy popular hike is
dog-friendly but hellish for a nervous rescue. A long exposed ridge is dog-friendly but dangerous for a brachycephalic
breed in summer. AllTrails’ filter cannot see any of that.
</p>
<p>
This is exactly the problem TailTrails is built to solve — and the idea behind our article on
{' '}<Link to="/blog/why-dog-friendly-isnt-t-enough-and-what-actually-matters" className="text-brand">why "dog-friendly" isn’t enough</Link>.
</p>
<h2>What TailTrails does instead</h2>
<p>
TailTrails tags every trail with the things that actually decide whether a walk goes well — environment, terrain,
traffic, exposure, shade — and combines that with a profile of your dog (energy, sociability, confidence,
sensitivities). The result is a shortlist of walks that bring out the best in your dog, not just a list of places
dogs are tolerated. It also records your walks and lets you save and revisit favourites.
</p>
<p>TailTrails is the stronger choice if:</p>
<ul>
<li>Your dog has specific needs (reactive, high-energy, older, sensitive)</li>
<li>You want trail suggestions, not just a filter on a database</li>
<li>You want a dog-first community, not a general hiking app with a dog tag</li>
</ul>
<h2>Cost and offline</h2>
<p>
AllTrails is free to browse but gates offline maps, wrong-turn alerts and 3D previews behind a paid subscription.
TailTrails is free. If your main use is finding the right dog walk near you rather than backcountry navigation,
the free tier of TailTrails covers it without an upsell.
</p>
<h2>Which should you pick?</h2>
<p><strong>Choose AllTrails if</strong> you are a serious hiker first and a dog owner second, and you want the biggest trail catalogue with a dog filter on top.</p>
<p><strong>Choose TailTrails if</strong> the dog is the point — you want walks chosen because they suit your dog, not just because dogs are allowed.</p>
</article>
<div className="mt-10">
<Link to="/compare" className="text-brand font-semibold hover:underline">← Back to all comparisons</Link>
</div>
</div>
</section>
</main>
<Footer />
</div>
);
export default TailtrailsVsAlltrails;
@@ -0,0 +1,80 @@
import { Link } from 'react-router-dom';
import Header from '@/components/landing/Header';
import Footer from '@/components/landing/Footer';
import SEO, { makeComparisonSchema } from '@/components/SEO';
import '../blog.css';
const TailtrailsVsFitbark = () => (
<div className="min-h-screen bg-background">
<SEO
title="TailTrails vs FitBark"
description="FitBark is a GPS and health collar with a subscription; TailTrails is a free phone app for walk discovery and recording. Different goals, different costs. See which fits your dog."
keywords={["tailtrails vs fitbark", "fitbark alternative", "dog activity tracker vs walk app", "free dog walking app"]}
jsonLd={makeComparisonSchema('FitBark', 'https://www.fitbark.com', 'tailtrails-vs-fitbark')}
/>
<Header />
<main className="pt-32 pb-20">
<section className="container mx-auto px-6">
<div className="max-w-3xl mx-auto">
<h1 className="text-3xl md:text-4xl font-bold text-text-primary mb-6">TailTrails vs FitBark</h1>
<p className="text-lg text-text-secondary mb-8">
FitBark and TailTrails both live on a dog owner’s phone, but they come at the same question from opposite ends.
FitBark sells you a collar and a subscription to monitor your dog’s location, activity and health. TailTrails is a
free app that helps you find and record the right walks. One is a health-and-safety device; the other is a walking
companion.
</p>
<article className="post-content max-w-none text-text-primary">
<h2>What FitBark is built for</h2>
<p>
FitBark is a GPS and activity collar (plus a mandatory subscription for the cellular service). It tracks live
location, escape alerts from “Safe Places”, location history, activity, sleep quality, and a Health Index it uses
to flag early signs of illness or anxiety. It is used by owners and vets, and it integrates with Apple Health,
Google Fit and Fitbit. It is a serious health-monitoring device.
</p>
<p>FitBark is the right choice if your priority is:</p>
<ul>
<li>Continuous health, sleep and activity monitoring for your dog</li>
<li>Live GPS location and escape alerts</li>
<li>Sharing data with a vet, trainer or dog walker</li>
</ul>
<h2>What TailTrails is built for</h2>
<p>
TailTrails does not monitor your dog’s body and it cannot find a lost dog. What it does is answer the question
FitBark never asks: <strong>where should I walk my dog today?</strong> It suggests trails matched to your dog’s
energy, behaviour and needs using community tags, records your walks with GPS, and lets you save and revisit
favourites. No collar, no subscription.
</p>
<p>TailTrails is the stronger choice if your priority is:</p>
<ul>
<li>Discovering new walks that suit your dog, not just logging them</li>
<li>A free app with no hardware or ongoing cost</li>
<li>A dog-first community sharing trails and routes</li>
</ul>
<h2>The cost gap</h2>
<p>
FitBark requires a device purchase and a subscription — the collar’s tracking features do not work without it.
Over a few years that is a meaningful ongoing cost, and it buys safety and health insight, not walk discovery.
TailTrails is free. If your dog is healthy and not an escape risk, you are paying FitBark for capabilities you may
not need.
</p>
<h2>Which should you pick?</h2>
<p><strong>Choose FitBark if</strong> you want health and location monitoring for your dog, especially an older, anxious or escape-prone dog.</p>
<p><strong>Choose TailTrails if</strong> you want to find better walks for your dog and record them, for free.</p>
<p>They pair well: FitBark to keep tabs on your dog’s wellbeing, TailTrails to actually decide where the two of you go.</p>
</article>
<div className="mt-10">
<Link to="/compare" className="text-brand font-semibold hover:underline">← Back to all comparisons</Link>
</div>
</div>
</section>
</main>
<Footer />
</div>
);
export default TailtrailsVsFitbark;
@@ -0,0 +1,78 @@
import { Link } from 'react-router-dom';
import Header from '@/components/landing/Header';
import Footer from '@/components/landing/Footer';
import SEO, { makeComparisonSchema } from '@/components/SEO';
import '../blog.css';
const TailtrailsVsPitpat = () => (
<div className="min-h-screen bg-background">
<SEO
title="TailTrails vs PitPat"
description="PitPat is a UK activity and GPS device for your dog’s collar; TailTrails is a free app with no hardware. One tracks exercise and location, the other finds the right walks. Which suits your routine?"
keywords={["tailtrails vs pitpat", "pitpat alternative", "dog activity tracker vs walk app", "free dog walking app uk"]}
jsonLd={makeComparisonSchema('PitPat', 'https://pitpat.com', 'tailtrails-vs-pitpat')}
/>
<Header />
<main className="pt-32 pb-20">
<section className="container mx-auto px-6">
<div className="max-w-3xl mx-auto">
<h1 className="text-3xl md:text-4xl font-bold text-text-primary mb-6">TailTrails vs PitPat</h1>
<p className="text-lg text-text-secondary mb-8">
PitPat is a popular UK-made device that clips to your dog’s collar and tracks activity (and, on the GPS model,
location). TailTrails is a free phone app that helps you find and record walks. Like the other hardware trackers,
PitPat and TailTrails solve different problems — and the device-vs-app distinction matters for your wallet.
</p>
<article className="post-content max-w-none text-text-primary">
<h2>What PitPat is built for</h2>
<p>
PitPat’s strength is exercise and activity data. Its activity monitor tracks daily movement, calories, rest and
breed-specific exercise goals, with a battery that lasts months. The PitPat GPS model adds location tracking with a
no-subscription option — pay once, no monthly fee — which is unusual in this category. It is a tidy,
low-running-cost way to know how much exercise your dog is actually getting.
</p>
<p>PitPat is the right choice if you want:</p>
<ul>
<li>Long-term activity and exercise data for your dog</li>
<li>A no-subscription GPS option (on the GPS model)</li>
<li>A “how much is my dog moving?” answer rather than a “where should we walk?” one</li>
</ul>
<h2>What TailTrails is built for</h2>
<p>
PitPat tells you how much your dog exercised. It does not tell you where to take them — it records activity, not
routes you might enjoy, and it does not match trails to your dog’s temperament. TailTrails fills that gap: it
suggests walks suited to your dog’s energy, behaviour and sensitivities, records them with your phone’s GPS, and
lets you save favourites. No device, no collar, no subscription.
</p>
<p>TailTrails is the stronger choice if you want:</p>
<ul>
<li>Trail ideas matched to your dog, not just an activity total</li>
<li>A free app with nothing to charge or attach</li>
<li>Variety in your walking routine</li>
</ul>
<h2>The cost and friction difference</h2>
<p>
Even PitPat’s no-subscription GPS is still a device you buy, charge (or replace the battery on), and keep attached
to the collar. TailTrails is an app you already have everything you need to use — your phone. If your goal is
better walks rather than measured exercise, the device adds cost and chores without helping with the decision.
</p>
<h2>Which should you pick?</h2>
<p><strong>Choose PitPat if</strong> you want to know how much exercise your dog is getting, or a no-monthly-fee GPS for peace of mind.</p>
<p><strong>Choose TailTrails if</strong> you want to find new walks that suit your dog and record them, for free.</p>
<p>They are complementary: PitPat quantifies your dog’s day, TailTrails decides where that day happens.</p>
</article>
<div className="mt-10">
<Link to="/compare" className="text-brand font-semibold hover:underline">← Back to all comparisons</Link>
</div>
</div>
</section>
</main>
<Footer />
</div>
);
export default TailtrailsVsPitpat;
@@ -0,0 +1,91 @@
import { Link } from 'react-router-dom';
import Header from '@/components/landing/Header';
import Footer from '@/components/landing/Footer';
import SEO, { makeComparisonSchema } from '@/components/SEO';
import '../blog.css';
const TailtrailsVsTractive = () => (
<div className="min-h-screen bg-background">
<SEO
title="TailTrails vs Tractive"
description="Tractive is a GPS collar for finding a lost dog; TailTrails is a free app for finding the right walk. Different problems — here is which you need, and why many owners use both."
keywords={["tailtrails vs tractive", "tractive alternative", "dog walk app vs gps tracker", "dog walking app"]}
jsonLd={makeComparisonSchema('Tractive', 'https://tractive.com', 'tailtrails-vs-tractive')}
/>
<Header />
<main className="pt-32 pb-20">
<section className="container mx-auto px-6">
<div className="max-w-3xl mx-auto">
<h1 className="text-3xl md:text-4xl font-bold text-text-primary mb-6">TailTrails vs Tractive</h1>
<p className="text-lg text-text-secondary mb-8">
TailTrails and Tractive both show up when you search for "dog walking app," but they solve completely different
problems. Confusing them is the most common — and most expensive — mistake in this category. Here is the short
version: Tractive tells you <em>where your dog is</em>. TailTrails tells you <em>where to walk your dog</em>.
</p>
<article className="post-content max-w-none text-text-primary">
<h2>The fundamental difference</h2>
<p>
<strong>Tractive</strong> is a GPS collar plus a mandatory subscription. Its job is live location: real-time tracking,
geofence escape alerts, and location history. It also has a "Walk" feature that records your route using your phone’s
GPS (the standalone Tractive Dog Walk app has been retired and folded into the main app). But make no mistake —
Tractive is a safety device first.
</p>
<p>
<strong>TailTrails</strong> is a free phone app. Its job is walk discovery: it suggests trails matched to your dog’s
traits, records your walks, and lets you save and share routes. It has no view of where your dog is when off the lead.
It is an everyday-walking app first.
</p>
<h2>Where Tractive is strong</h2>
<p>
If your dog has ever bolted, slips the lead, or you walk in open countryside where they vanish out of sight, Tractive
is excellent — live updates every couple of seconds and instant alerts when they leave a safe zone. That peace of
mind is what you are paying for. Its activity and sleep tracking is a nice secondary benefit.
</p>
<p>Tractive is the right call if your real worry is:</p>
<ul>
<li>"My dog might get lost or escape"</li>
<li>You walk off-lead in remote or open areas</li>
<li>You want escape alerts, not walk ideas</li>
</ul>
<h2>Where TailTrails is different</h2>
<p>
Tractive will not tell you which trail suits a reactive dog, where the quiet forest paths are, or which beach is low
traffic at this time of year. It records a walk you already decided to do; it does not help you decide. And it costs
money every month — the collar is useless without the subscription.
</p>
<p>
TailTrails is free, needs no hardware, and is built around the decision Tractive does not help with: <strong>where
should I take my dog today?</strong> It matches trails to your dog’s profile so you stop defaulting to the same
route, and it records the walk once you are out.
</p>
<h2>The cost reality</h2>
<p>
TailTrails is free. Tractive is a device purchase plus an ongoing subscription — over two or three years that adds
up considerably. That cost buys safety features, not discovery. If lost-dog recovery is not your concern, you are
paying for capability you may not use.
</p>
<h2>Which should you pick?</h2>
<p><strong>Choose Tractive if</strong> your dog is an escape risk or you need live location and escape alerts.</p>
<p><strong>Choose TailTrails if</strong> you want to find and record better walks for your dog, for free.</p>
<p>
Many owners genuinely benefit from <em>both</em>: a GPS collar for safety on off-lead adventures, and a free app like
TailTrails to actually decide where those adventures happen. They are complementary, not competitors.
</p>
</article>
<div className="mt-10">
<Link to="/compare" className="text-brand font-semibold hover:underline">← Back to all comparisons</Link>
</div>
</div>
</section>
</main>
<Footer />
</div>
);
export default TailtrailsVsTractive;
@@ -0,0 +1,90 @@
import { Link } from 'react-router-dom';
import Header from '@/components/landing/Header';
import Footer from '@/components/landing/Footer';
import SEO, { makeComparisonSchema } from '@/components/SEO';
import '../blog.css';
const TailtrailsVsWooftrax = () => (
<div className="min-h-screen bg-background">
<SEO
title="TailTrails vs WoofTrax (Walk for a Dog)"
description="Compare TailTrails vs WoofTrax: two free dog walking apps. WoofTrax donates to shelters per walk; TailTrails matches trails to your dog and helps you discover new ones. Which fits you?"
keywords={["tailtrails vs wooftrax", "walk for a dog app", "free dog walking app", "wooftrax alternative"]}
jsonLd={makeComparisonSchema('WoofTrax', 'https://wooftrax.com', 'tailtrails-vs-wooftrax')}
/>
<Header />
<main className="pt-32 pb-20">
<section className="container mx-auto px-6">
<div className="max-w-3xl mx-auto">
<h1 className="text-3xl md:text-4xl font-bold text-text-primary mb-6">TailTrails vs WoofTrax (Walk for a Dog)</h1>
<p className="text-lg text-text-secondary mb-8">
TailTrails and WoofTrax are both free phone apps that record your dog walks with GPS — no hardware, no subscription.
They are the two most natural alternatives if you want a simple "track my dog walk" app. But they are built around
very different ideas about what a dog walk is for.
</p>
<article className="post-content max-w-none text-text-primary">
<h2>What each app is built around</h2>
<p>
<strong>WoofTrax (Walk for a Dog)</strong> is built around charity. Every walk you log earns donations for the animal
shelter or rescue of your choice, funded by sponsors and advertising. The walk itself is recorded — distance,
duration, route — and you can join groups and challenges. The mission is getting more people walking so more
money reaches shelters.
</p>
<p>
<strong>TailTrails</strong> is built around discovery. It also records your walks (distance, duration, route, photos),
but its core idea is that the <em>same</em> daily walk gets boring and that most "dog-friendly" places are not actually
right for your dog. So it suggests new trails matched to your dog’s energy, behaviour and needs, using community
tags for environment, terrain, traffic and exposure.
</p>
<h2>Where WoofTrax is strong</h2>
<p>
WoofTrax has been around a long time and has logged a huge number of walks for a huge number of charities. If you
already walk your dog the same routes every day and simply want that habit to do some good, it is hard to argue with
— you press start, you walk, a shelter benefits. The groups and challenges add a nice social layer, and the app
is genuinely free.
</p>
<p>WoofTrax is a great fit if you want:</p>
<ul>
<li>To support a specific shelter or rescue with your daily walk</li>
<li>Group walking challenges with friends or a community</li>
<li>A straightforward walk log without much decision-making</li>
</ul>
<h2>Where TailTrails is different</h2>
<p>
The gap WoofTrax does not fill is <strong>"where should I walk today?"</strong> It records where you went, but it does
not help you find a new place that suits your dog. If your reactive dog needs a quiet trail, or your spaniel needs
open space to run, WoofTrax cannot tell you which routes to try.
</p>
<p>
TailTrails does. Every trail is tagged by the community, every dog has a profile, and the app brings the two together
so you can choose with confidence instead of guessing. You can also save trails and follow them again. It is the
stronger choice if variety and the right environment matter to you and your dog.
</p>
<h2>Potential downsides of each</h2>
<p>
WoofTrax is ad-funded, which some owners find intrusive, and its trail discovery is effectively non-existent.
TailTrails is newer, so its community trail coverage is still growing in regions outside its launch areas — the
more people use it and tag trails, the better the matching gets.
</p>
<h2>Which should you pick?</h2>
<p><strong>Choose WoofTrax if</strong> your main motivation is supporting a shelter with walks you already do.</p>
<p><strong>Choose TailTrails if</strong> you want to discover new walks that genuinely suit your dog, not just log the ones you already know.</p>
<p>And nothing stops you using both — but if you had to pick one for everyday use, it comes down to charity vs discovery.</p>
</article>
<div className="mt-10">
<Link to="/compare" className="text-brand font-semibold hover:underline">← Back to all comparisons</Link>
</div>
</div>
</section>
</main>
<Footer />
</div>
);
export default TailtrailsVsWooftrax;