Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
078b57795b | ||
|
|
68a388cd78 | ||
|
|
61fd457af0 | ||
|
|
bde9dbc48d | ||
|
|
1fd092a9f0 | ||
|
|
8d1b953c61 | ||
|
|
497002a112 | ||
|
|
b28c002d7d | ||
|
|
59ffde396f | ||
|
|
29ff175e89 | ||
|
|
2b0e89191a | ||
|
|
9d14652a57 | ||
|
|
634f4fcdf1 | ||
|
|
bd0881f882 | ||
|
|
c83186f809 | ||
|
|
1f8179d562 | ||
|
|
cdf166632a | ||
|
|
1b0198913c | ||
|
|
a6ada39784 | ||
|
|
bb963850b1 | ||
|
|
28d95b4dcd | ||
|
|
5d2dc11fbc | ||
|
|
8319a74f99 | ||
|
|
5cf58ffa8a | ||
|
|
7dfd5aec9b | ||
|
|
768dc1151a | ||
|
|
1a1b62136f | ||
|
|
f0a2c8d745 | ||
|
|
53e9ea999d | ||
|
|
092f1e44a2 | ||
|
|
17f171d3d9 | ||
|
|
5535f10e7d | ||
|
|
a0ef97b48d | ||
|
|
f3e2812694 | ||
|
|
80bf4bc019 | ||
|
|
a8c5c1837d | ||
|
|
04199bd606 | ||
|
|
570085cb04 | ||
|
|
d440d3fa15 | ||
|
|
2677881a88 | ||
|
|
68dd48e506 | ||
|
|
5aa4782de7 | ||
|
|
99245764a9 | ||
|
|
5ce14ea666 | ||
|
|
e067fec168 | ||
|
|
ff42a242db | ||
|
|
ada9605c90 | ||
|
|
babc4642ce | ||
|
|
d69a3da26e | ||
|
|
1c3a2a4b0e | ||
|
|
4d5d59f3bd | ||
|
|
c55c707694 | ||
|
|
63e43ce7a1 | ||
|
|
832bb41206 | ||
|
|
64540e208f | ||
|
|
c74315e950 | ||
|
|
e8c93d89ad | ||
|
|
82b74c359a | ||
|
|
5d65fa7027 | ||
|
|
c6509e9920 | ||
|
|
0e832d8fd1 | ||
|
|
2aa9b19c3c | ||
|
|
bc942cdb7c | ||
|
|
8f72652ada | ||
|
|
812571b177 | ||
|
|
f2e375a8f1 | ||
|
|
d8a6126b9d | ||
|
|
bcd0ef890a | ||
|
|
2359e87a35 | ||
|
|
9e38e05aad | ||
|
|
0f8ae45e59 | ||
|
|
95d5d455b9 | ||
|
|
7497fbf588 | ||
|
|
6d486ad954 | ||
|
|
ae97f4f5eb | ||
|
|
f119f041f0 | ||
|
|
8fc647826f | ||
|
|
b71d6bddb9 | ||
|
|
ec6e496900 | ||
|
|
46b943bffe | ||
|
|
ef0ea506c2 | ||
|
|
0a493d4e10 | ||
|
|
e6f0b6a395 | ||
|
|
283983012f | ||
|
|
3e2a7a71a6 | ||
|
|
4801e09466 | ||
|
|
02b7b4f18c | ||
|
|
3d8fe194be | ||
|
|
db8b2b1c63 | ||
|
|
9715a3b01f | ||
|
|
b42f754525 |
No files matched your search
@@ -28,8 +28,7 @@
|
||||
onload="this.media='all'"
|
||||
/>
|
||||
<noscript><link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Marcellus&display=swap" /></noscript>
|
||||
<link rel="stylesheet" href="style.css" media="print" onload="this.media='all'" />
|
||||
<noscript><link rel="stylesheet" href="style.css" /></noscript>
|
||||
<link rel="stylesheet" href="style.css?v=061517baef" />
|
||||
<style>
|
||||
/* ---- Australia landing page (page-scoped; additive) ---- */
|
||||
.cmp-sr-only {
|
||||
@@ -392,7 +391,7 @@
|
||||
window.plausible=window.plausible||function(){(plausible.q=plausible.q||[]).push(arguments)},plausible.init=plausible.init||function(i){plausible.o=i||{}};
|
||||
plausible.init()
|
||||
</script>
|
||||
<script defer src="/static/lang.js"></script>
|
||||
<script defer src="/static/lang.js?v=3ec49a7e62"></script>
|
||||
</head>
|
||||
<body>
|
||||
<!-- Page-scoped top bar: the site has no global header, so the page carries its own way home. -->
|
||||
@@ -408,6 +407,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Hero -->
|
||||
<main>
|
||||
<section class="hero cmp-hero">
|
||||
<div class="hero-bg">
|
||||
<img
|
||||
@@ -733,6 +733,7 @@
|
||||
|
||||
<!-- Footer (copied from index.html; icon-font glyphs replaced with inline SVG so the page
|
||||
carries no external icon dependency) -->
|
||||
</main>
|
||||
<footer class="footer">
|
||||
<div class="container">
|
||||
<div class="fgrid">
|
||||
@@ -741,6 +742,7 @@
|
||||
<ul>
|
||||
<li><a href="/odoo-erp-automation-bootcamp/">ODOO Bootcamp</a></li>
|
||||
<li><a href="odoo-hosting-2027.html">ODOO Hosting in 2027</a></li>
|
||||
<li><a href="/odoo-community-developers/">Odoo Freelancer Directory</a></li>
|
||||
<li><a href="support.html">Support</a></li>
|
||||
<li><a href="legal.html">Legal</a></li>
|
||||
</ul>
|
||||
@@ -764,7 +766,7 @@
|
||||
<a href="https://www.linkedin.com/company/odoo4projects/" target="_blank" aria-label="LinkedIn"><svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M4.98 3.5a2.5 2.5 0 1 1 0 5 2.5 2.5 0 0 1 0-5zM3 9h4v12H3zM9 9h3.8v1.7h.05c.53-.95 1.83-1.95 3.76-1.95 4.02 0 4.76 2.5 4.76 5.75V21h-4v-5.5c0-1.31-.02-3-1.9-3-1.9 0-2.19 1.42-2.19 2.9V21H9z" /></svg></a>
|
||||
<a href="https://www.youtube.com/@ODOO4projects" target="_blank" aria-label="YouTube"><svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M23 12s0-3.2-.41-4.73a2.5 2.5 0 0 0-1.76-1.77C19.3 5.1 12 5.1 12 5.1s-7.3 0-8.83.4A2.5 2.5 0 0 0 1.4 7.27C1 8.8 1 12 1 12s0 3.2.41 4.73a2.5 2.5 0 0 0 1.76 1.77c1.53.4 8.83.4 8.83.4s7.3 0 8.83-.4a2.5 2.5 0 0 0 1.76-1.77C23 15.2 23 12 23 12zM9.75 15.02V8.98L15.5 12z" /></svg></a>
|
||||
</div>
|
||||
<form class="footer-contact-form" id="footerContactForm" action="mailto:hello@odoo4projects.com" method="post" enctype="text/plain">
|
||||
<form class="footer-contact-form" id="footerContactForm" action="mailto:support@odoo4projects.com" method="post" enctype="text/plain">
|
||||
<label class="cmp-sr-only" for="footerContactEmail">Your email address</label>
|
||||
<input type="email" id="footerContactEmail" name="email" placeholder="Your email address" required />
|
||||
<div class="footer-textarea-wrap">
|
||||
|
||||
@@ -1,5 +1,41 @@
|
||||
{
|
||||
"posts": [
|
||||
{
|
||||
"id": "your-agent-can-run-your-odoo-hosting",
|
||||
"date": "2026-11-05",
|
||||
"area": "news",
|
||||
"headline": "Your Agent Can Run Your Odoo Hosting: the MCP Server Is Live",
|
||||
"teaser": "This is the piece of the Odoo 20 update we are asked about most: an MCP server that lets your agent work on your Odoo ho",
|
||||
"link": "blog/posts/your-agent-can-run-your-odoo-hosting.html",
|
||||
"image": "https://image.office.odoo4projects.com/img/20_4server.png"
|
||||
},
|
||||
{
|
||||
"id": "freelancer-directory-soft-launch",
|
||||
"date": "2026-11-04",
|
||||
"area": "news",
|
||||
"headline": "Our freelancer directory opens on 1 December. Help us get the signup right",
|
||||
"teaser": "We need your help! We are opening our Odoo Community Freelancer directory on 1 December. Before real clients start brows",
|
||||
"link": "blog/posts/freelancer-directory-soft-launch.html",
|
||||
"image": "https://image.office.odoo4projects.com/img/freelance_platform.png"
|
||||
},
|
||||
{
|
||||
"id": "odoo-20-is-live-on-our-servers-here-is-what-changed",
|
||||
"date": "2026-11-03",
|
||||
"area": "news",
|
||||
"headline": "Odoo 20 Is Live on Our Servers — Here Is What Changed",
|
||||
"teaser": "Two weeks. That is how long it took us to bring up Odoo 20 on our servers, with every new feature that comes with it. It",
|
||||
"link": "blog/posts/odoo-20-is-live-on-our-servers-here-is-what-changed.html",
|
||||
"image": "https://image.office.odoo4projects.com/img/ODOO_20_chatter.png"
|
||||
},
|
||||
{
|
||||
"id": "from-a-spreadsheet-to-a-running-erp",
|
||||
"date": "2026-11-02",
|
||||
"area": "news",
|
||||
"headline": "From a Spreadsheet to a Running ERP in Ten Thursday Sessions",
|
||||
"teaser": "Most small businesses do not run on an ERP. They run on a spreadsheet, a messaging group, and one person's memory. It wo",
|
||||
"link": "blog/posts/from-a-spreadsheet-to-a-running-erp.html",
|
||||
"image": "https://image.office.odoo4projects.com/img/screencast.png"
|
||||
},
|
||||
{
|
||||
"id": "oca-paraguay-internationalization-hackathon",
|
||||
"date": "2026-10-29",
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=Inter+Tight:wght@500;600;700;800&display=swap" rel="stylesheet" />
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css" />
|
||||
<link rel="stylesheet" href="../../style.css" />
|
||||
<link rel="stylesheet" href="../../style.css?v=061517baef" />
|
||||
<style>
|
||||
body { background: #0d1b2e; }
|
||||
.blog-post { max-width: 720px; margin: 0 auto; padding: 3rem 1.5rem 6rem; color: #171c26; background: #faf7f1; min-height: 100vh; }
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
rel="stylesheet"
|
||||
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css"
|
||||
/>
|
||||
<link rel="stylesheet" href="../../style.css" />
|
||||
<link rel="stylesheet" href="../../style.css?v=061517baef" />
|
||||
<style>
|
||||
:root {
|
||||
--primary: #714b67;
|
||||
@@ -134,6 +134,7 @@
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<div class="container">
|
||||
<div class="blog-header">
|
||||
<a href="../../index.html#blog">← Back to Blog</a>
|
||||
@@ -323,6 +324,7 @@
|
||||
<a href="../../index.html#blog">← Back to all posts</a>
|
||||
</div>
|
||||
</div>
|
||||
<script defer src="/static/agent-odoo4projects/agent.js"></script>
|
||||
</main>
|
||||
<script defer src="/static/agent-odoo4projects/agent.js?v=c3bb0cee2b"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,83 @@
|
||||
<!doctype html>
|
||||
<html lang="en-US">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Our freelancer directory opens on 1 December. Help us get the signup right | ODOO4projects</title>
|
||||
<meta name="description" content="We need your help! We are opening our Odoo Community Freelancer directory on 1 December. Before real clients start brows" />
|
||||
<link rel="canonical" href="https://odoo4projects.com/blog/posts/freelancer-directory-soft-launch.html" />
|
||||
<meta property="og:title" content="Our freelancer directory opens on 1 December. Help us get the signup right" />
|
||||
<meta property="og:description" content="We need your help! We are opening our Odoo Community Freelancer directory on 1 December. Before real clients start brows" />
|
||||
<meta property="og:url" content="https://odoo4projects.com/blog/posts/freelancer-directory-soft-launch.html" />
|
||||
<meta property="og:type" content="article" />
|
||||
<meta property="og:image" content="https://image.office.odoo4projects.com/img/freelance_platform.png" />
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<link rel="icon" href="../../images/favicon.ico" />
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=Inter+Tight:wght@500;600;700;800&display=swap" rel="stylesheet" />
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css" />
|
||||
<link rel="stylesheet" href="../../style.css?v=061517baef" />
|
||||
<style>
|
||||
body { background: #0d1b2e; }
|
||||
.blog-post { max-width: 720px; margin: 0 auto; padding: 3rem 1.5rem 6rem; color: #171c26; background: #faf7f1; min-height: 100vh; }
|
||||
.blog-post h1 { font-family: 'Marcellus', Georgia, serif; font-size: 2.4rem; font-weight: 400; margin: 0 0 0.5rem; color: #0d1b2e; }
|
||||
.blog-post .post-meta { font-family: Inter, sans-serif; font-size: 0.82rem; color: rgba(23,28,38,0.6); margin-bottom: 2rem; }
|
||||
.blog-post .post-meta .badge { display: inline-block; font-size: 0.68rem; font-weight: 600; letter-spacing: 0.15em; text-transform: uppercase; color: #c9a227; margin-right: 0.75rem; }
|
||||
.blog-post .post-image { width: 100%; border-radius: 0.75rem; margin-bottom: 2rem; box-shadow: 0 2px 8px rgba(13,27,46,0.08); }
|
||||
.blog-post p { font-family: Inter, sans-serif; font-size: 1rem; line-height: 1.7; margin-bottom: 1.2rem; }
|
||||
.blog-post h2 { font-family: 'Marcellus', Georgia, serif; font-size: 1.6rem; font-weight: 400; margin: 2rem 0 0.75rem; color: #0d1b2e; }
|
||||
.blog-post a { color: #8a6f1f; text-decoration: underline; }
|
||||
.blog-post strong { font-weight: 600; }
|
||||
.back-link { display: inline-block; margin-top: 2rem; font-family: 'Marcellus', Georgia, serif; font-size: 0.9rem; text-transform: uppercase; letter-spacing: 0.04em; color: #c9a227; text-decoration: none; }
|
||||
.back-link:hover { text-decoration: underline; }
|
||||
@media (max-width: 600px) { .blog-post h1 { font-size: 1.7rem; } .blog-post { padding: 2rem 1rem 4rem; } }
|
||||
</style>
|
||||
<script type="application/ld+json">
|
||||
{
|
||||
"@context": "https://schema.org",
|
||||
"@type": "BlogPosting",
|
||||
"headline": "Our freelancer directory opens on 1 December. Help us get the signup right",
|
||||
"datePublished": "2026-11-04",
|
||||
"url": "https://odoo4projects.com/blog/posts/freelancer-directory-soft-launch.html",
|
||||
"image": "https://image.office.odoo4projects.com/img/freelance_platform.png",
|
||||
"author": {
|
||||
"@type": "Organization",
|
||||
"name": "ODOO4projects",
|
||||
"url": "https://odoo4projects.com/"
|
||||
},
|
||||
"publisher": {
|
||||
"@type": "Organization",
|
||||
"name": "ODOO4projects",
|
||||
"url": "https://odoo4projects.com/",
|
||||
"logo": {
|
||||
"@type": "ImageObject",
|
||||
"url": "https://odoo4projects.com/images/logo.png"
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<!-- Privacy-friendly analytics by Plausible -->
|
||||
<script async src="https://plausible.office.odoo4projects.com/js/pa-NhA7Rf-79VopXFQXhSS4e.js"></script>
|
||||
<script>
|
||||
window.plausible=window.plausible||function(){(plausible.q=plausible.q||[]).push(arguments)},plausible.init=plausible.init||function(i){plausible.o=i||{}};
|
||||
plausible.init()
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<main class="blog-post">
|
||||
<span class="badge">news</span>
|
||||
<h1>Our freelancer directory opens on 1 December. Help us get the signup right</h1>
|
||||
<div class="post-meta">November 4, 2026 · News</div>
|
||||
<img src="https://image.office.odoo4projects.com/img/freelance_platform.png" alt="Our freelancer directory opens on 1 December. Help us get the signup right" class="post-image" loading="lazy" />
|
||||
<div class="post-body">
|
||||
<p>We need your help!<br><br>We are opening our <b>Odoo Community Freelancer directory</b> on 1 December. Before real clients start browsing it, we want the signup to work, and we would like your help to get it right.</p>
|
||||
<p>So this is a soft launch. Sign up for free and tell us how the signup went: where you got stuck, what you expected to find, what you would change. That feedback is what we fix before opening day.</p>
|
||||
<p>Joining is free, for freelancers and clients both. The directory is where a freelancer describes their work, in their own words.</p>
|
||||
<p>If you are a member of the Odoo Community Association, this is a chance to be seen alongside your community. Your profile card carries an OCA Member Profile badge, and a visitor can filter the directory by OCA membership. We mark it, and the choice of who to contact stays with the visitor.</p>
|
||||
<p>Start here: <a href="https://odoo4projects.com/feedback/">odoo4projects.com/feedback</a></p>
|
||||
</div>
|
||||
<a href="../../index.html#blog" class="back-link">← Back to the Blog</a>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,97 @@
|
||||
<!doctype html>
|
||||
<html lang="en-US">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>From a Spreadsheet to a Running ERP in Ten Thursday Sessions | ODOO4projects</title>
|
||||
<meta name="description" content="Most small businesses do not run on an ERP. They run on a spreadsheet, a messaging group, and one person's memory. It works — until that person is on holiday." />
|
||||
<link rel="canonical" href="https://odoo4projects.com/blog/posts/from-a-spreadsheet-to-a-running-erp.html" />
|
||||
<meta property="og:title" content="From a Spreadsheet to a Running ERP in Ten Thursday Sessions" />
|
||||
<meta property="og:description" content="Most small businesses do not run on an ERP. They run on a spreadsheet, a messaging group, and one person's memory. It works — until that person is on holiday." />
|
||||
<meta property="og:type" content="article" />
|
||||
<meta property="og:image" content="https://image.office.odoo4projects.com/img/screencast.png" />
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<link rel="icon" href="../../images/favicon.ico" />
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=Inter+Tight:wght@500;600;700;800&display=swap" rel="stylesheet" />
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css" />
|
||||
<link rel="stylesheet" href="../../style.css?v=061517baef" />
|
||||
<style>
|
||||
body { background: #0d1b2e; }
|
||||
.blog-post { max-width: 720px; margin: 0 auto; padding: 3rem 1.5rem 6rem; color: #171c26; background: #faf7f1; min-height: 100vh; }
|
||||
.blog-post h1 { font-family: 'Marcellus', Georgia, serif; font-size: 2.4rem; font-weight: 400; margin: 0 0 0.5rem; color: #0d1b2e; }
|
||||
.blog-post .post-meta { font-family: Inter, sans-serif; font-size: 0.82rem; color: rgba(23,28,38,0.6); margin-bottom: 2rem; }
|
||||
.blog-post .post-meta .badge { display: inline-block; font-size: 0.68rem; font-weight: 600; letter-spacing: 0.15em; text-transform: uppercase; color: #c9a227; margin-right: 0.75rem; }
|
||||
.blog-post .post-image { width: 100%; border-radius: 0.75rem; margin-bottom: 2rem; box-shadow: 0 2px 8px rgba(13,27,46,0.08); }
|
||||
.blog-post p { font-family: Inter, sans-serif; font-size: 1rem; line-height: 1.7; margin-bottom: 1.2rem; }
|
||||
.blog-post ul { font-family: Inter, sans-serif; font-size: 1rem; line-height: 1.7; margin-bottom: 1.2rem; padding-left: 1.5rem; }
|
||||
.blog-post li { margin-bottom: 0.3rem; }
|
||||
.blog-post a { color: #8a6f1f; text-decoration: underline; }
|
||||
.blog-post strong { font-weight: 600; }
|
||||
.back-link { display: inline-block; margin-top: 2rem; font-family: 'Marcellus', Georgia, serif; font-size: 0.9rem; text-transform: uppercase; letter-spacing: 0.04em; color: #c9a227; text-decoration: none; }
|
||||
.back-link:hover { text-decoration: underline; }
|
||||
@media (max-width: 600px) { .blog-post h1 { font-size: 1.7rem; } .blog-post { padding: 2rem 1rem 4rem; } }
|
||||
</style>
|
||||
<script type="application/ld+json">
|
||||
{
|
||||
"@context": "https://schema.org",
|
||||
"@type": "BlogPosting",
|
||||
"headline": "From a Spreadsheet to a Running ERP in Ten Thursday Sessions",
|
||||
"datePublished": "2026-11-02",
|
||||
"dateModified": "2026-11-02",
|
||||
"url": "https://odoo4projects.com/blog/posts/from-a-spreadsheet-to-a-running-erp.html",
|
||||
"image": "https://image.office.odoo4projects.com/img/screencast.png",
|
||||
"author": {
|
||||
"@type": "Organization",
|
||||
"name": "ODOO4projects",
|
||||
"url": "https://odoo4projects.com/"
|
||||
},
|
||||
"publisher": {
|
||||
"@type": "Organization",
|
||||
"name": "ODOO4projects",
|
||||
"url": "https://odoo4projects.com/",
|
||||
"logo": {
|
||||
"@type": "ImageObject",
|
||||
"url": "https://odoo4projects.com/images/logo.png"
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<!-- Privacy-friendly analytics by Plausible -->
|
||||
<script async src="https://plausible.office.odoo4projects.com/js/pa-NhA7Rf-79VopXFQXhSS4e.js"></script>
|
||||
<script>
|
||||
window.plausible=window.plausible||function(){(plausible.q=plausible.q||[]).push(arguments)},plausible.init=plausible.init||function(i){plausible.o=i||{}};
|
||||
plausible.init()
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<main class="blog-post">
|
||||
<span class="badge">news</span>
|
||||
<h1>From a Spreadsheet to a Running ERP in Ten Thursday Sessions</h1>
|
||||
<div class="post-meta">November 2, 2026 · News</div>
|
||||
<img src="https://image.office.odoo4projects.com/img/screencast.png" alt="From a Spreadsheet to a Running ERP in Ten Thursday Sessions" class="post-image" loading="lazy" />
|
||||
<div class="post-body">
|
||||
<p>Most small businesses do not run on an ERP. They run on a spreadsheet, a messaging group, and one person's memory. It works — until that person is on holiday.</p>
|
||||
|
||||
<p>The <strong>Odoo ERP Automation Bootcamp</strong> is ten live Thursday sessions where you build a working system for your own company: your products, your customers, your quotes, your invoices, your stock.</p>
|
||||
|
||||
<p>No programming. No server to install. If you can run your business on a spreadsheet, you can do this.</p>
|
||||
|
||||
<p><strong>What you get</strong></p>
|
||||
<ul>
|
||||
<li>Ten live sessions, one hour each — Thursdays, 18:00–19:00 Central Time</li>
|
||||
<li>Starts Thursday 21 January 2027</li>
|
||||
<li>You build on a live system from the first session, not on slides</li>
|
||||
<li><strong>One year of hosted Odoo Community Edition included</strong> — you automate on a real instance, not a sandbox you set up once and forget</li>
|
||||
<li>A group capped at six, so everyone gets seen</li>
|
||||
</ul>
|
||||
|
||||
<p>You finish with an ERP you actually use, and the judgement to keep it running yourself.</p>
|
||||
|
||||
<p>USD 595.00. Details and the full ten lessons: <a href="https://odoo4projects.com/odoo-erp-automation-bootcamp/?utm_source=linkedin&utm_medium=social&utm_campaign=bootcamp-jan2027">odoo4projects.com/odoo-erp-automation-bootcamp</a></p>
|
||||
</div>
|
||||
<a href="../../index.html#blog" class="back-link">← Back to the Blog</a>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
@@ -36,7 +36,7 @@
|
||||
rel="stylesheet"
|
||||
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css"
|
||||
/>
|
||||
<link rel="stylesheet" href="../../style.css" />
|
||||
<link rel="stylesheet" href="../../style.css?v=061517baef" />
|
||||
<style>
|
||||
.blog-header {
|
||||
padding: 2rem 0;
|
||||
@@ -203,6 +203,7 @@
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<div class="container">
|
||||
<div class="blog-header">
|
||||
<a href="../../index.html#blog">← Back to Blog</a>
|
||||
@@ -273,6 +274,7 @@
|
||||
<a href="../../index.html#blog">← Back to all posts</a>
|
||||
</div>
|
||||
</div>
|
||||
<script defer src="/static/agent-odoo4projects/agent.js"></script>
|
||||
</main>
|
||||
<script defer src="/static/agent-odoo4projects/agent.js?v=c3bb0cee2b"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -16,7 +16,7 @@
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=Inter+Tight:wght@500;600;700;800&display=swap" rel="stylesheet" />
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css" />
|
||||
<link rel="stylesheet" href="../../style.css" />
|
||||
<link rel="stylesheet" href="../../style.css?v=061517baef" />
|
||||
<style>
|
||||
body { background: #0d1b2e; }
|
||||
.blog-post { max-width: 720px; margin: 0 auto; padding: 3rem 1.5rem 6rem; color: #171c26; background: #faf7f1; min-height: 100vh; }
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=Inter+Tight:wght@500;600;700;800&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<link rel="stylesheet" href="../../style.css" />
|
||||
<link rel="stylesheet" href="../../style.css?v=061517baef" />
|
||||
<style>
|
||||
/* Blog post specific overrides — matching homepage light theme */
|
||||
body {
|
||||
@@ -190,6 +190,7 @@
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<div class="blog-post-container">
|
||||
<div class="blog-header">
|
||||
<a href="../../index.html#blog">← Back to Blog</a>
|
||||
@@ -253,5 +254,6 @@
|
||||
<a href="../../index.html#blog">← Back to all posts</a>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
@@ -36,7 +36,7 @@
|
||||
rel="stylesheet"
|
||||
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css"
|
||||
/>
|
||||
<link rel="stylesheet" href="../../style.css" />
|
||||
<link rel="stylesheet" href="../../style.css?v=061517baef" />
|
||||
<style>
|
||||
:root {
|
||||
--primary: #714b67;
|
||||
@@ -214,6 +214,7 @@
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<div class="container">
|
||||
<div class="blog-header">
|
||||
<a href="../../index.html#blog">← Back to Blog</a>
|
||||
@@ -275,6 +276,7 @@
|
||||
<a href="../../index.html#blog">← Back to all posts</a>
|
||||
</div>
|
||||
</div>
|
||||
<script defer src="/static/agent-odoo4projects/agent.js"></script>
|
||||
</main>
|
||||
<script defer src="/static/agent-odoo4projects/agent.js?v=c3bb0cee2b"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -36,7 +36,7 @@
|
||||
rel="stylesheet"
|
||||
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css"
|
||||
/>
|
||||
<link rel="stylesheet" href="../../style.css" />
|
||||
<link rel="stylesheet" href="../../style.css?v=061517baef" />
|
||||
<style>
|
||||
:root {
|
||||
--primary: #714b67;
|
||||
@@ -214,6 +214,7 @@
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<div class="container">
|
||||
<div class="blog-header">
|
||||
<a href="../../index.html#blog">← Back to Blog</a>
|
||||
@@ -294,6 +295,7 @@ Admin credentials: sent via email</code></pre>
|
||||
<a href="../../index.html#blog">← Back to all posts</a>
|
||||
</div>
|
||||
</div>
|
||||
<script defer src="/static/agent-odoo4projects/agent.js"></script>
|
||||
</main>
|
||||
<script defer src="/static/agent-odoo4projects/agent.js?v=c3bb0cee2b"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -35,7 +35,7 @@
|
||||
rel="stylesheet"
|
||||
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css"
|
||||
/>
|
||||
<link rel="stylesheet" href="../../style.css" />
|
||||
<link rel="stylesheet" href="../../style.css?v=061517baef" />
|
||||
<style>
|
||||
:root {
|
||||
--primary: #714b67;
|
||||
@@ -196,6 +196,7 @@
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<div class="container">
|
||||
<div class="blog-header">
|
||||
<a href="../../index.html#blog">← Back to Blog</a>
|
||||
@@ -259,9 +260,10 @@
|
||||
<a href="../../index.html#blog">← Back to all posts</a>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
<script
|
||||
defer
|
||||
src="/static/agent-odoo4projects/agent.js"
|
||||
src="/static/agent-odoo4projects/agent.js?v=c3bb0cee2b"
|
||||
></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -36,7 +36,7 @@
|
||||
rel="stylesheet"
|
||||
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css"
|
||||
/>
|
||||
<link rel="stylesheet" href="../../style.css" />
|
||||
<link rel="stylesheet" href="../../style.css?v=061517baef" />
|
||||
<style>
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
@@ -197,6 +197,7 @@
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<div class="container">
|
||||
<div class="blog-header">
|
||||
<a href="../../index.html#blog">← Back to Blog</a>
|
||||
@@ -259,9 +260,10 @@
|
||||
<a href="../../index.html#blog">← Back to all posts</a>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
<script
|
||||
defer
|
||||
src="/static/agent-odoo4projects/agent.js"
|
||||
src="/static/agent-odoo4projects/agent.js?v=c3bb0cee2b"
|
||||
></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -16,7 +16,7 @@
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=Inter+Tight:wght@500;600;700;800&display=swap" rel="stylesheet" />
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css" />
|
||||
<link rel="stylesheet" href="../../style.css" />
|
||||
<link rel="stylesheet" href="../../style.css?v=061517baef" />
|
||||
<style>
|
||||
body { background: #0d1b2e; }
|
||||
.blog-post { max-width: 720px; margin: 0 auto; padding: 3rem 1.5rem 6rem; color: #171c26; background: #faf7f1; min-height: 100vh; }
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
rel="stylesheet"
|
||||
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css"
|
||||
/>
|
||||
<link rel="stylesheet" href="../../style.css" />
|
||||
<link rel="stylesheet" href="../../style.css?v=061517baef" />
|
||||
<style>
|
||||
:root {
|
||||
--primary: #714b67;
|
||||
@@ -107,6 +107,7 @@
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<div class="container">
|
||||
<div class="blog-header">
|
||||
<a href="../../index.html#blog">← Back to Blog</a>
|
||||
@@ -173,6 +174,7 @@
|
||||
<a href="../../index.html#blog">← Back to all posts</a>
|
||||
</div>
|
||||
</div>
|
||||
<script defer src="/static/agent-odoo4projects/agent.js"></script>
|
||||
</main>
|
||||
<script defer src="/static/agent-odoo4projects/agent.js?v=c3bb0cee2b"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,101 @@
|
||||
<!doctype html>
|
||||
<html lang="en-US">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Odoo 20 Is Live on Our Servers — Here Is What Changed | ODOO4projects</title>
|
||||
<meta name="description" content="Two weeks. That is how long it took us to bring up Odoo 20 on our servers, with every new feature that comes with it. It" />
|
||||
<link rel="canonical" href="https://odoo4projects.com/blog/posts/odoo-20-is-live-on-our-servers-here-is-what-changed.html" />
|
||||
<meta property="og:title" content="Odoo 20 Is Live on Our Servers — Here Is What Changed" />
|
||||
<meta property="og:description" content="Two weeks. That is how long it took us to bring up Odoo 20 on our servers, with every new feature that comes with it. It" />
|
||||
<meta property="og:url" content="https://odoo4projects.com/blog/posts/odoo-20-is-live-on-our-servers-here-is-what-changed.html" />
|
||||
<meta property="og:type" content="article" />
|
||||
<meta property="og:image" content="https://image.office.odoo4projects.com/img/ODOO_20_chatter.png" />
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<link rel="icon" href="../../images/favicon.ico" />
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=Inter+Tight:wght@500;600;700;800&display=swap" rel="stylesheet" />
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css" />
|
||||
<link rel="stylesheet" href="../../style.css?v=061517baef" />
|
||||
<style>
|
||||
body { background: #0d1b2e; }
|
||||
.blog-post { max-width: 720px; margin: 0 auto; padding: 3rem 1.5rem 6rem; color: #171c26; background: #faf7f1; min-height: 100vh; }
|
||||
.blog-post h1 { font-family: 'Marcellus', Georgia, serif; font-size: 2.4rem; font-weight: 400; margin: 0 0 0.5rem; color: #0d1b2e; }
|
||||
.blog-post .post-meta { font-family: Inter, sans-serif; font-size: 0.82rem; color: rgba(23,28,38,0.6); margin-bottom: 2rem; }
|
||||
.blog-post .post-meta .badge { display: inline-block; font-size: 0.68rem; font-weight: 600; letter-spacing: 0.15em; text-transform: uppercase; color: #c9a227; margin-right: 0.75rem; }
|
||||
.blog-post .post-image { width: 100%; border-radius: 0.75rem; margin-bottom: 2rem; box-shadow: 0 2px 8px rgba(13,27,46,0.08); }
|
||||
.blog-post p { font-family: Inter, sans-serif; font-size: 1rem; line-height: 1.7; margin-bottom: 1.2rem; }
|
||||
.blog-post h2 { font-family: 'Marcellus', Georgia, serif; font-size: 1.6rem; font-weight: 400; margin: 2rem 0 0.75rem; color: #0d1b2e; }
|
||||
.blog-post ul, .blog-post ol { font-family: Inter, sans-serif; font-size: 1rem; line-height: 1.7; margin-bottom: 1.2rem; padding-left: 1.5rem; }
|
||||
.blog-post li { margin-bottom: 0.3rem; }
|
||||
.blog-post a { color: #8a6f1f; text-decoration: underline; }
|
||||
.blog-post strong { font-weight: 600; }
|
||||
.blog-post figure { margin: 2rem 0; }
|
||||
.blog-post figure img { width: 100%; border-radius: 0.75rem; box-shadow: 0 2px 8px rgba(13,27,46,0.08); }
|
||||
.blog-post figcaption { font-family: Inter, sans-serif; font-size: 0.85rem; line-height: 1.6; color: rgba(23,28,38,0.6); margin-top: 0.6rem; }
|
||||
.back-link { display: inline-block; margin-top: 2rem; font-family: 'Marcellus', Georgia, serif; font-size: 0.9rem; text-transform: uppercase; letter-spacing: 0.04em; color: #c9a227; text-decoration: none; }
|
||||
.back-link:hover { text-decoration: underline; }
|
||||
@media (max-width: 600px) { .blog-post h1 { font-size: 1.7rem; } .blog-post { padding: 2rem 1rem 4rem; } }
|
||||
</style>
|
||||
<script type="application/ld+json">
|
||||
{
|
||||
"@context": "https://schema.org",
|
||||
"@type": "BlogPosting",
|
||||
"headline": "Odoo 20 Is Live on Our Servers — Here Is What Changed",
|
||||
"datePublished": "2026-11-03",
|
||||
"url": "https://odoo4projects.com/blog/posts/odoo-20-is-live-on-our-servers-here-is-what-changed.html",
|
||||
"image": "https://image.office.odoo4projects.com/img/ODOO_20_chatter.png",
|
||||
"author": {
|
||||
"@type": "Organization",
|
||||
"name": "ODOO4projects",
|
||||
"url": "https://odoo4projects.com/"
|
||||
},
|
||||
"publisher": {
|
||||
"@type": "Organization",
|
||||
"name": "ODOO4projects",
|
||||
"url": "https://odoo4projects.com/",
|
||||
"logo": {
|
||||
"@type": "ImageObject",
|
||||
"url": "https://odoo4projects.com/images/logo.png"
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<!-- Privacy-friendly analytics by Plausible -->
|
||||
<script async src="https://plausible.office.odoo4projects.com/js/pa-NhA7Rf-79VopXFQXhSS4e.js"></script>
|
||||
<script>
|
||||
window.plausible=window.plausible||function(){(plausible.q=plausible.q||[]).push(arguments)},plausible.init=plausible.init||function(i){plausible.o=i||{}};
|
||||
plausible.init()
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<main class="blog-post">
|
||||
<span class="badge">news</span>
|
||||
<h1>Odoo 20 Is Live on Our Servers — Here Is What Changed</h1>
|
||||
<div class="post-meta">November 3, 2026 · News</div>
|
||||
<img src="https://image.office.odoo4projects.com/img/ODOO_20_chatter.png" alt="Odoo 20 Is Live on Our Servers — Here Is What Changed" class="post-image" loading="lazy" />
|
||||
<div class="post-body">
|
||||
<p>Two weeks. That is how long it took us to bring up <strong>Odoo 20</strong> on our servers, with every new feature that comes with it. It is live now — if your instance runs with us, the engine underneath it just moved forward.</p>
|
||||
|
||||
<p>The version upgrade is only part of the story. Alongside it we shipped a set of changes to the management console, the place where you run your Odoo hosting from inside Odoo itself.</p>
|
||||
|
||||
<h2>What landed</h2>
|
||||
|
||||
<ul>
|
||||
<li><strong>An MCP server</strong> so your agent can work on your hosting directly — restart the server, upload modules, add and check domains, manage backups, read the logfiles.</li>
|
||||
<li><strong>Logfiles you can keep</strong> — download and archive them instead of watching a live view and hoping the right lines are still there.</li>
|
||||
<li><strong>A free backup scheme for everyone</strong> — the least glamorous part of hosting, and the one you miss most when it is gone.</li>
|
||||
<li><strong>A better AI server-management backend</strong> — the layer that connects the agent-facing tools to the hosting underneath.</li>
|
||||
</ul>
|
||||
|
||||
<h2>Why it matters</h2>
|
||||
|
||||
<p>Nothing here asks you to learn a new product. The console stays where it was; the upgrade makes it do more of the routine work for you, and the parts you would rather not think about — backups, logs, restarts — now have a baseline everyone gets.</p>
|
||||
|
||||
<p>Over the next two weeks we take one piece at a time: the MCP server first, then the quieter half — backups and logfiles. Follow along.</p>
|
||||
</div>
|
||||
<a href="../../index.html#blog" class="back-link">← Back to the Blog</a>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
@@ -16,7 +16,7 @@
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=Inter+Tight:wght@500;600;700;800&display=swap" rel="stylesheet" />
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css" />
|
||||
<link rel="stylesheet" href="../../style.css" />
|
||||
<link rel="stylesheet" href="../../style.css?v=061517baef" />
|
||||
<style>
|
||||
body { background: #0d1b2e; }
|
||||
.blog-post { max-width: 720px; margin: 0 auto; padding: 3rem 1.5rem 6rem; color: #171c26; background: #faf7f1; min-height: 100vh; }
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=Inter+Tight:wght@500;600;700;800&display=swap" rel="stylesheet" />
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css" />
|
||||
<link rel="stylesheet" href="../../style.css" />
|
||||
<link rel="stylesheet" href="../../style.css?v=061517baef" />
|
||||
<style>
|
||||
body { background: #0d1b2e; }
|
||||
.blog-post { max-width: 720px; margin: 0 auto; padding: 3rem 1.5rem 6rem; color: #171c26; background: #faf7f1; min-height: 100vh; }
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
rel="stylesheet"
|
||||
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css"
|
||||
/>
|
||||
<link rel="stylesheet" href="../../style.css" />
|
||||
<link rel="stylesheet" href="../../style.css?v=061517baef" />
|
||||
<style>
|
||||
/* Blog post specific styles — leverages CSS vars from style.css */
|
||||
:root {
|
||||
@@ -173,6 +173,7 @@
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<div class="container" style="width:80%; max-width:800px; margin:0 auto; padding:0 20px;">
|
||||
<div class="blog-header">
|
||||
<a href="../../index.html#blog">← Back to Blog</a>
|
||||
@@ -309,6 +310,7 @@
|
||||
<a href="../../index.html#blog">← Back to all posts</a>
|
||||
</div>
|
||||
</div>
|
||||
<script defer src="/static/agent-odoo4projects/agent.js"></script>
|
||||
</main>
|
||||
<script defer src="/static/agent-odoo4projects/agent.js?v=c3bb0cee2b"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -35,7 +35,7 @@
|
||||
rel="stylesheet"
|
||||
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css"
|
||||
/>
|
||||
<link rel="stylesheet" href="../../style.css" />
|
||||
<link rel="stylesheet" href="../../style.css?v=061517baef" />
|
||||
<style>
|
||||
.container {
|
||||
width: 80%;
|
||||
@@ -181,6 +181,7 @@
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<div class="container">
|
||||
<div class="blog-header">
|
||||
<a href="../../index.html#blog">← Back to Blog</a>
|
||||
@@ -248,6 +249,7 @@ Odoo > Settings > Technical > Scheduled Actions > backup job</code><
|
||||
<a href="../../index.html#blog">← Back to all posts</a>
|
||||
</div>
|
||||
</div>
|
||||
<script defer src="/static/agent-odoo4projects/agent.js"></script>
|
||||
</main>
|
||||
<script defer src="/static/agent-odoo4projects/agent.js?v=c3bb0cee2b"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -35,7 +35,7 @@
|
||||
rel="stylesheet"
|
||||
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css"
|
||||
/>
|
||||
<link rel="stylesheet" href="../../style.css" />
|
||||
<link rel="stylesheet" href="../../style.css?v=061517baef" />
|
||||
<style>
|
||||
:root {
|
||||
--primary: #714b67;
|
||||
@@ -202,6 +202,7 @@
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<div class="container">
|
||||
<div class="blog-header">
|
||||
<a href="../../index.html#blog">← Back to Blog</a>
|
||||
@@ -265,6 +266,7 @@ Password: (check your email)</code></pre>
|
||||
<a href="../../index.html#blog">← Back to all posts</a>
|
||||
</div>
|
||||
</div>
|
||||
<script defer src="/static/agent-odoo4projects/agent.js"></script>
|
||||
</main>
|
||||
<script defer src="/static/agent-odoo4projects/agent.js?v=c3bb0cee2b"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -34,7 +34,7 @@
|
||||
rel="stylesheet"
|
||||
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css"
|
||||
/>
|
||||
<link rel="stylesheet" href="../../style.css" />
|
||||
<link rel="stylesheet" href="../../style.css?v=061517baef" />
|
||||
<style>
|
||||
:root {
|
||||
--primary: #714b67;
|
||||
@@ -107,6 +107,7 @@
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<div class="container">
|
||||
<div class="blog-header">
|
||||
<a href="../../index.html#blog">← Back to Blog</a>
|
||||
@@ -163,6 +164,7 @@
|
||||
<a href="../../index.html#blog">← Back to all posts</a>
|
||||
</div>
|
||||
</div>
|
||||
<script defer src="/static/agent-odoo4projects/agent.js"></script>
|
||||
</main>
|
||||
<script defer src="/static/agent-odoo4projects/agent.js?v=c3bb0cee2b"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,97 @@
|
||||
<!doctype html>
|
||||
<html lang="en-US">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Your Agent Can Run Your Odoo Hosting: the MCP Server Is Live | ODOO4projects</title>
|
||||
<meta name="description" content="This is the piece of the Odoo 20 update we are asked about most: an MCP server that lets your agent work on your Odoo ho" />
|
||||
<link rel="canonical" href="https://odoo4projects.com/blog/posts/your-agent-can-run-your-odoo-hosting.html" />
|
||||
<meta property="og:title" content="Your Agent Can Run Your Odoo Hosting: the MCP Server Is Live" />
|
||||
<meta property="og:description" content="This is the piece of the Odoo 20 update we are asked about most: an MCP server that lets your agent work on your Odoo ho" />
|
||||
<meta property="og:url" content="https://odoo4projects.com/blog/posts/your-agent-can-run-your-odoo-hosting.html" />
|
||||
<meta property="og:type" content="article" />
|
||||
<meta property="og:image" content="https://image.office.odoo4projects.com/img/20_4server.png" />
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<link rel="icon" href="../../images/favicon.ico" />
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=Inter+Tight:wght@500;600;700;800&display=swap" rel="stylesheet" />
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css" />
|
||||
<link rel="stylesheet" href="../../style.css?v=061517baef" />
|
||||
<style>
|
||||
body { background: #0d1b2e; }
|
||||
.blog-post { max-width: 720px; margin: 0 auto; padding: 3rem 1.5rem 6rem; color: #171c26; background: #faf7f1; min-height: 100vh; }
|
||||
.blog-post h1 { font-family: 'Marcellus', Georgia, serif; font-size: 2.4rem; font-weight: 400; margin: 0 0 0.5rem; color: #0d1b2e; }
|
||||
.blog-post .post-meta { font-family: Inter, sans-serif; font-size: 0.82rem; color: rgba(23,28,38,0.6); margin-bottom: 2rem; }
|
||||
.blog-post .post-meta .badge { display: inline-block; font-size: 0.68rem; font-weight: 600; letter-spacing: 0.15em; text-transform: uppercase; color: #c9a227; margin-right: 0.75rem; }
|
||||
.blog-post .post-image { width: 100%; border-radius: 0.75rem; margin-bottom: 2rem; box-shadow: 0 2px 8px rgba(13,27,46,0.08); }
|
||||
.blog-post p { font-family: Inter, sans-serif; font-size: 1rem; line-height: 1.7; margin-bottom: 1.2rem; }
|
||||
.blog-post h2 { font-family: 'Marcellus', Georgia, serif; font-size: 1.6rem; font-weight: 400; margin: 2rem 0 0.75rem; color: #0d1b2e; }
|
||||
.blog-post ul, .blog-post ol { font-family: Inter, sans-serif; font-size: 1rem; line-height: 1.7; margin-bottom: 1.2rem; padding-left: 1.5rem; }
|
||||
.blog-post li { margin-bottom: 0.3rem; }
|
||||
.blog-post a { color: #8a6f1f; text-decoration: underline; }
|
||||
.blog-post strong { font-weight: 600; }
|
||||
.back-link { display: inline-block; margin-top: 2rem; font-family: 'Marcellus', Georgia, serif; font-size: 0.9rem; text-transform: uppercase; letter-spacing: 0.04em; color: #c9a227; text-decoration: none; }
|
||||
.back-link:hover { text-decoration: underline; }
|
||||
@media (max-width: 600px) { .blog-post h1 { font-size: 1.7rem; } .blog-post { padding: 2rem 1rem 4rem; } }
|
||||
</style>
|
||||
<script type="application/ld+json">
|
||||
{
|
||||
"@context": "https://schema.org",
|
||||
"@type": "BlogPosting",
|
||||
"headline": "Your Agent Can Run Your Odoo Hosting: the MCP Server Is Live",
|
||||
"datePublished": "2026-11-05",
|
||||
"url": "https://odoo4projects.com/blog/posts/your-agent-can-run-your-odoo-hosting.html",
|
||||
"image": "https://image.office.odoo4projects.com/img/20_4server.png",
|
||||
"author": {
|
||||
"@type": "Organization",
|
||||
"name": "ODOO4projects",
|
||||
"url": "https://odoo4projects.com/"
|
||||
},
|
||||
"publisher": {
|
||||
"@type": "Organization",
|
||||
"name": "ODOO4projects",
|
||||
"url": "https://odoo4projects.com/",
|
||||
"logo": {
|
||||
"@type": "ImageObject",
|
||||
"url": "https://odoo4projects.com/images/logo.png"
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<!-- Privacy-friendly analytics by Plausible -->
|
||||
<script async src="https://plausible.office.odoo4projects.com/js/pa-NhA7Rf-79VopXFQXhSS4e.js"></script>
|
||||
<script>
|
||||
window.plausible=window.plausible||function(){(plausible.q=plausible.q||[]).push(arguments)},plausible.init=plausible.init||function(i){plausible.o=i||{}};
|
||||
plausible.init()
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<main class="blog-post">
|
||||
<span class="badge">news</span>
|
||||
<h1>Your Agent Can Run Your Odoo Hosting: the MCP Server Is Live</h1>
|
||||
<div class="post-meta">November 5, 2026 · News</div>
|
||||
<img src="https://image.office.odoo4projects.com/img/20_4server.png" alt="Your Agent Can Run Your Odoo Hosting: the MCP Server Is Live" class="post-image" loading="lazy" />
|
||||
<div class="post-body">
|
||||
<p>This is the piece of the Odoo 20 update we are asked about most: an <strong>MCP server</strong> that lets your agent work on your Odoo hosting directly, instead of you clicking through menus to do routine things.</p>
|
||||
|
||||
<h2>What your agent can do</h2>
|
||||
|
||||
<ul>
|
||||
<li><strong>Restart the server</strong> — the task nobody wants to be paged for.</li>
|
||||
<li><strong>Upload modules</strong> — deploy new functionality as part of the same workflow.</li>
|
||||
<li><strong>Add and check domains</strong> — routing handled where the work happens.</li>
|
||||
<li><strong>Manage your backups</strong> — create them, and reinstall one when you need to.</li>
|
||||
<li><strong>Read the logfiles</strong> — investigate a problem without digging through them by hand.</li>
|
||||
</ul>
|
||||
|
||||
<h2>Why we built it this way</h2>
|
||||
|
||||
<p>Hosting chores are short, frequent and interrupt you at the worst moment. Handing them to an agent changes the shape of the day: you stay on the work that moves your project, and the operational tasks happen in the background — under the same account, on the same console you already use.</p>
|
||||
|
||||
<p>The console itself is unchanged. What changed is that the routine half of running an instance no longer needs you in front of it.</p>
|
||||
</div>
|
||||
<a href="../../index.html#blog" class="back-link">← Back to the Blog</a>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
@@ -128,7 +128,9 @@
|
||||
.hero-stats{display:flex;gap:1.5rem;margin-bottom:2rem;flex-wrap:wrap}.hero-stat{display:flex;align-items:center;gap:.4rem;color:rgba(255,255,255,.65);font-size:.85rem}.hero-stat i{color:var(--brass);font-size:.75rem}
|
||||
.hero-actions{display:flex;gap:1rem;flex-wrap:wrap;margin-bottom:2rem}
|
||||
.hero-notice{position:absolute;top:1.25rem;right:1.25rem;z-index:3;display:flex;align-items:center;gap:.55rem;max-width:min(360px,calc(100% - 2rem));padding:.6rem .85rem .6rem 1rem;background:#f5c542;color:var(--midnight);border:1px solid rgba(13,27,46,.14);border-radius:var(--radius);box-shadow:0 8px 24px rgba(13,27,46,.3);font-family:Inter,sans-serif;font-size:.82rem;line-height:1.4;animation:heroNoticeIn .4s cubic-bezier(.16,1,.3,1)}
|
||||
.hero-notice[hidden]{display:none}
|
||||
/* `.hero-notice[hidden]{display:none}` used to live here; the site-wide `[hidden]`
|
||||
rule in /style.css hides it (`.hero-notice` sets `display:flex`, which outranked
|
||||
the UA rule). */
|
||||
.hero-notice i{color:var(--midnight);font-size:.9rem;flex-shrink:0}
|
||||
.hero-notice-text{flex:1;min-width:0;overflow-wrap:anywhere}
|
||||
.hero-notice-close{flex-shrink:0;display:flex;align-items:center;justify-content:center;width:1.35rem;height:1.35rem;padding:0;border:0;border-radius:50%;background:rgba(13,27,46,.12);color:var(--midnight);font-size:1rem;line-height:1;cursor:pointer;transition:background .2s}
|
||||
@@ -220,15 +222,14 @@
|
||||
#customQuoteModal .biz-success p{color:var(--midnight)}
|
||||
@media(max-width:600px){.oca-table td,.oca-table th{padding:.5rem .65rem;font-size:.8rem}.ow-panel{left:5%;right:5%}.ow-close{top:.5rem;right:.5rem;font-size:1.6rem}}
|
||||
</style>
|
||||
<link rel="stylesheet" href="/style.css" media="print" onload="this.media='all'" />
|
||||
<noscript><link rel="stylesheet" href="/style.css" /></noscript>
|
||||
<link rel="stylesheet" href="/style.css?v=061517baef" />
|
||||
<!-- Privacy-friendly analytics by Plausible -->
|
||||
<script async src="https://plausible.office.odoo4projects.com/js/pa-NhA7Rf-79VopXFQXhSS4e.js"></script>
|
||||
<script>
|
||||
window.plausible=window.plausible||function(){(plausible.q=plausible.q||[]).push(arguments)},plausible.init=plausible.init||function(i){plausible.o=i||{}};
|
||||
plausible.init()
|
||||
</script>
|
||||
<script defer src="/static/lang.js"></script>
|
||||
<script defer src="/static/lang.js?v=3ec49a7e62"></script>
|
||||
</head>
|
||||
<body>
|
||||
<!--
|
||||
@@ -240,6 +241,7 @@
|
||||
FINISH: unreviewed and undocumented is unfinished; this build ends with the finish review, the verdict, and DESIGN.md.
|
||||
-->
|
||||
<!-- Hero -->
|
||||
<main>
|
||||
<section class="hero">
|
||||
<div class="hero-notice" id="heroConfirmNotice" role="status" aria-live="polite" hidden>
|
||||
<i class="fas fa-check-circle" aria-hidden="true"></i>
|
||||
@@ -496,7 +498,7 @@
|
||||
<h2 class="ow-title" id="orderPlanTitle">On the Rise</h2>
|
||||
<p class="ow-price">
|
||||
<span class="ow-price-amount" id="orderPlanPrice">$ 395.00</span> <span id="orderPlanPeriod">/ Jahr</span>
|
||||
<span class="ow-price-tax" id="orderPlanTax">+ MwSt.</span>
|
||||
<span class="ow-price-tax" id="orderPlanTax">inkl. MwSt.</span>
|
||||
</p>
|
||||
<p class="ow-coupon-msg" id="orderCouponMsg">Fragen vor dem Kauf? Fragen Sie den Assistenten auf dieser Seite.</p>
|
||||
<p class="ow-yearly-save" id="orderYearlySave">Jährliche Abrechnung. Keine Einrichtungsgebühr.</p>
|
||||
@@ -625,6 +627,7 @@
|
||||
</section>
|
||||
|
||||
<!-- Footer -->
|
||||
</main>
|
||||
<footer class="footer">
|
||||
<div class="container">
|
||||
<div class="fgrid">
|
||||
@@ -671,7 +674,7 @@
|
||||
</footer>
|
||||
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.5/gsap.min.js"></script>
|
||||
<script src="/script.js"></script>
|
||||
<script defer src="/static/agent-odoo4projects/agent.js"></script>
|
||||
<script src="/script.js?v=d2046e3469"></script>
|
||||
<script defer src="/static/agent-odoo4projects/agent.js?v=c3bb0cee2b"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -38,8 +38,7 @@
|
||||
onload="this.media='all'"
|
||||
/>
|
||||
<noscript><link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Marcellus&display=swap" /></noscript>
|
||||
<link rel="stylesheet" href="/style.css" media="print" onload="this.media='all'" />
|
||||
<noscript><link rel="stylesheet" href="/style.css" /></noscript>
|
||||
<link rel="stylesheet" href="/style.css?v=061517baef" />
|
||||
<style>
|
||||
/* ---- Odoo Community Upgrade page (page-scoped; additive) ---- */
|
||||
.oc-sr-only {
|
||||
@@ -480,7 +479,7 @@
|
||||
window.plausible=window.plausible||function(){(plausible.q=plausible.q||[]).push(arguments)},plausible.init=plausible.init||function(i){plausible.o=i||{}};
|
||||
plausible.init()
|
||||
</script>
|
||||
<script defer src="/static/lang.js"></script>
|
||||
<script defer src="/static/lang.js?v=3ec49a7e62"></script>
|
||||
</head>
|
||||
<body>
|
||||
<!-- Page-scoped top bar: the site has no global header, so the page carries its own way home. -->
|
||||
@@ -496,6 +495,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Hero -->
|
||||
<main>
|
||||
<section class="hero oc-hero">
|
||||
<div class="hero-bg">
|
||||
<img
|
||||
@@ -837,6 +837,7 @@ sudo -u postgres pg_restore -d <DB_NEW> /path/backup/<DB>.dump
|
||||
|
||||
<!-- Footer (copied from index.html; icon-font glyphs replaced with inline SVG so the page
|
||||
carries no external icon dependency) -->
|
||||
</main>
|
||||
<footer class="footer">
|
||||
<div class="container">
|
||||
<div class="fgrid">
|
||||
@@ -868,7 +869,7 @@ sudo -u postgres pg_restore -d <DB_NEW> /path/backup/<DB>.dump
|
||||
<a href="https://www.linkedin.com/company/odoo4projects/" target="_blank" aria-label="LinkedIn"><svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M4.98 3.5a2.5 2.5 0 1 1 0 5 2.5 2.5 0 0 1 0-5zM3 9h4v12H3zM9 9h3.8v1.7h.05c.53-.95 1.83-1.95 3.76-1.95 4.02 0 4.76 2.5 4.76 5.75V21h-4v-5.5c0-1.31-.02-3-1.9-3-1.9 0-2.19 1.42-2.19 2.9V21H9z" /></svg></a>
|
||||
<a href="https://www.youtube.com/@ODOO4projects" target="_blank" aria-label="YouTube"><svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M23 12s0-3.2-.41-4.73a2.5 2.5 0 0 0-1.76-1.77C19.3 5.1 12 5.1 12 5.1s-7.3 0-8.83.4A2.5 2.5 0 0 0 1.4 7.27C1 8.8 1 12 1 12s0 3.2.41 4.73a2.5 2.5 0 0 0 1.76 1.77c1.53.4 8.83.4 8.83.4s7.3 0 8.83-.4a2.5 2.5 0 0 0 1.76-1.77C23 15.2 23 12 23 12zM9.75 15.02V8.98L15.5 12z" /></svg></a>
|
||||
</div>
|
||||
<form class="footer-contact-form" id="footerContactForm" action="mailto:hello@odoo4projects.com" method="post" enctype="text/plain">
|
||||
<form class="footer-contact-form" id="footerContactForm" action="mailto:support@odoo4projects.com" method="post" enctype="text/plain">
|
||||
<label class="oc-sr-only" for="footerContactEmail">Ihre E-Mail-Adresse</label>
|
||||
<input type="email" id="footerContactEmail" name="email" placeholder="Ihre E-Mail-Adresse" required />
|
||||
<div class="footer-textarea-wrap">
|
||||
|
||||
@@ -31,8 +31,9 @@
|
||||
onload="this.media='all'"
|
||||
/>
|
||||
<noscript><link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Marcellus&display=swap" /></noscript>
|
||||
<link rel="stylesheet" href="/style.css" media="print" onload="this.media='all'" />
|
||||
<noscript><link rel="stylesheet" href="/style.css" /></noscript>
|
||||
<link rel="preload" as="image" href="/images/hero/bootcamp-hero-mobile-900.webp" media="(max-width: 640px)" fetchpriority="high" />
|
||||
<link rel="preload" as="image" href="/images/hero/bootcamp-hero-desktop-1920.webp" media="(min-width: 641px)" fetchpriority="high" />
|
||||
<link rel="stylesheet" href="/style.css?v=061517baef" />
|
||||
<style>
|
||||
/* ---- ODOO ERP Automation Bootcamp landing page (page-scoped; additive) ---- */
|
||||
.bc-sr-only {
|
||||
@@ -192,6 +193,14 @@
|
||||
form[action$="/shop/post_basket"] .btn {
|
||||
line-height: inherit;
|
||||
}
|
||||
/* At 0 seats /static/bootcamp-availability.js disables these Buy buttons; make the
|
||||
disabled state read as disabled — quiet dimming in the page's own CI, never an
|
||||
alert banner. */
|
||||
form[action$="/shop/post_basket"] .btn:disabled,
|
||||
form[action$="/shop/post_basket"] .btn[aria-disabled="true"] {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* Grids — auto-fit so no breakpoint is needed */
|
||||
.bc-list {
|
||||
@@ -223,7 +232,7 @@
|
||||
border-radius: var(--radius-lg);
|
||||
box-shadow: var(--shadow-rest);
|
||||
padding: 1.4rem 1.4rem 1.4rem 3.4rem;
|
||||
transition: box-shadow 0.25s, transform 0.25s;
|
||||
transition: transform 0.25s; /* box-shadow kept, not transitioned (non-composited) */
|
||||
}
|
||||
.bc-lessons li:hover {
|
||||
box-shadow: var(--shadow-hover);
|
||||
@@ -313,7 +322,7 @@
|
||||
.bc-schedule dt {
|
||||
font-family: Marcellus, Georgia, serif;
|
||||
font-size: 0.92rem;
|
||||
color: var(--brass-deep);
|
||||
color: #876c1e; /* 4.68:1 on --ivory (was --brass-deep 4.49:1) */
|
||||
}
|
||||
.bc-schedule dd {
|
||||
margin: 0;
|
||||
@@ -452,6 +461,109 @@
|
||||
.btn-brass:focus-visible {
|
||||
outline-color: #fff;
|
||||
}
|
||||
/* Waiting list — for someone who cannot attend the January cohort.
|
||||
Deliberately quieter than the Buy button: outline button, smaller type, no brass fill. */
|
||||
.bc-waitlist {
|
||||
/* Component-scoped error colour: the site has no shared token for it. */
|
||||
--wl-error: #b02a37;
|
||||
max-width: 720px;
|
||||
margin: 2.5rem auto 0;
|
||||
padding: 1.5rem;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--radius);
|
||||
background: var(--ivory);
|
||||
text-align: left;
|
||||
}
|
||||
.bc-wl-title {
|
||||
margin: 0 0 0.5rem;
|
||||
font-size: 1.1rem;
|
||||
color: var(--ink);
|
||||
}
|
||||
.bc-wl-lead {
|
||||
margin: 0 0 1.1rem;
|
||||
font-size: 0.88rem;
|
||||
line-height: 1.6;
|
||||
color: rgba(23, 28, 38, 0.78); /* the page's own .bc-note value, on the same cream */
|
||||
}
|
||||
.bc-wl-form input,
|
||||
.bc-wl-form textarea {
|
||||
display: block;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
margin-bottom: 0.6rem;
|
||||
padding: 0.6rem 0.75rem;
|
||||
font-family: Inter, sans-serif;
|
||||
font-size: 0.9rem;
|
||||
color: var(--ink);
|
||||
background: #fff;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--radius);
|
||||
}
|
||||
.bc-wl-form textarea {
|
||||
min-height: 3.4rem;
|
||||
resize: vertical;
|
||||
}
|
||||
.bc-wl-btn {
|
||||
padding: 0.6rem 1.5rem;
|
||||
font-size: 0.8rem;
|
||||
background: transparent;
|
||||
color: var(--ink);
|
||||
border-color: rgba(23, 28, 38, 0.35);
|
||||
}
|
||||
.bc-wl-btn:hover {
|
||||
border-color: var(--brass);
|
||||
background: rgba(201, 162, 39, 0.12);
|
||||
transform: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
.bc-wl-btn:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: default;
|
||||
}
|
||||
.bc-wl-success {
|
||||
margin: 0;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
color: var(--success);
|
||||
}
|
||||
.bc-wl-error {
|
||||
margin: 0;
|
||||
font-size: 0.9rem;
|
||||
color: var(--wl-error);
|
||||
}
|
||||
|
||||
/* --- 2026-11-02 PageSpeed pass (page-scoped, additive) ---------------------
|
||||
1. The footer <h4>s became <h3>s so the heading order descends after the last
|
||||
section h2 (a11y "heading-order"); these rules keep their look identical to
|
||||
the shared .footer h4 in /style.css.
|
||||
2. A link inside an on-dark note is the lighter brass (7.15:1 on midnight,
|
||||
A11y "color-contrast") and underlined, so it is distinguishable without
|
||||
colour (A11y "link-in-text-block"). */
|
||||
.footer h3 {
|
||||
font-family: Marcellus, Georgia, serif;
|
||||
color: #fff;
|
||||
margin-bottom: 0.8rem;
|
||||
font-size: 1rem;
|
||||
letter-spacing: 0.03em;
|
||||
}
|
||||
.bc-note.on-dark a {
|
||||
color: var(--brass);
|
||||
text-decoration: underline;
|
||||
}
|
||||
/* the footer copyright line at .35 alpha was 3.2:1 on midnight */
|
||||
.footer .fcopy {
|
||||
color: rgba(255, 255, 255, 0.5); /* 5.17:1 */
|
||||
}
|
||||
/* no-JS footer fallback (rendered only when JavaScript is off) */
|
||||
.footer .bc-noscript {
|
||||
margin-top: 0.6rem;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
.footer .bc-noscript a {
|
||||
color: var(--brass);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
</style>
|
||||
<script type="application/ld+json">
|
||||
{
|
||||
@@ -501,7 +613,7 @@
|
||||
},
|
||||
"offers": {
|
||||
"@type": "Offer",
|
||||
"price": "465.00",
|
||||
"price": "595.00",
|
||||
"priceCurrency": "USD",
|
||||
"category": "Paid",
|
||||
"url": "https://odoo4projects.com/odoo-erp-automation-bootcamp/"
|
||||
@@ -576,6 +688,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Hero -->
|
||||
<main>
|
||||
<section class="hero bc-hero">
|
||||
<div class="container">
|
||||
<p class="hero-kicker">Q1 2027 · 10 LIVE-SESSIONS · OLIVER ARNOLD</p>
|
||||
@@ -591,14 +704,21 @@
|
||||
</div>
|
||||
<div class="hero-actions">
|
||||
<form method="post" action="https://20.odoo4projects.com/shop/post_basket" target="_blank" class="plausible-event-name=Buy">
|
||||
<input type="hidden" name="basket" value='[{"product_id":12,"qty":1}]' />
|
||||
<input type="hidden" name="basket" value='[{"product_id":17,"qty":1,"info":"Q1 2027, English"}]' />
|
||||
<input type="hidden" name="clear" value="1" />
|
||||
<button type="submit" class="btn btn-brass btn-lg">Für 465.00 USD kaufen</button>
|
||||
<button type="submit" class="btn btn-brass btn-lg">Für 595.00 USD kaufen</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Live remaining-seat count, immediately below the hero. One seat element only on
|
||||
the page; /static/bootcamp-availability.js fills it from the availability webhook
|
||||
and leaves it empty+hidden if that fetch fails. -->
|
||||
<div class="container">
|
||||
<p class="bc-note bc-seats" data-bc-seats hidden></p>
|
||||
</div>
|
||||
|
||||
<!-- From zero to a running ERP -->
|
||||
<section class="section-light">
|
||||
<div class="container">
|
||||
@@ -612,9 +732,9 @@
|
||||
<p class="bc-subline">Drei Monate. Zehn Stunden Unterricht. Ein System, das Ihr Unternehmen steuert.</p>
|
||||
<div class="bc-action">
|
||||
<form method="post" action="https://20.odoo4projects.com/shop/post_basket" target="_blank" class="plausible-event-name=Buy">
|
||||
<input type="hidden" name="basket" value='[{"product_id":12,"qty":1}]' />
|
||||
<input type="hidden" name="basket" value='[{"product_id":17,"qty":1,"info":"Q1 2027, English"}]' />
|
||||
<input type="hidden" name="clear" value="1" />
|
||||
<button type="submit" class="btn btn-brass btn-lg">Für 465.00 USD kaufen</button>
|
||||
<button type="submit" class="btn btn-brass btn-lg">Für 595.00 USD kaufen</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
@@ -769,18 +889,37 @@
|
||||
<section class="section-light">
|
||||
<div class="container">
|
||||
<h2 class="section-title">Preis und Buchung</h2>
|
||||
<p class="bc-price">Das Programm kostet USD 465.00 für alle zehn Sitzungen.</p>
|
||||
<p class="bc-price">Das Programm kostet USD 595.00 für alle zehn Sitzungen.</p>
|
||||
<p class="bc-note"><strong>Begrenzt auf 6 Teilnehmer</strong> — die Gruppe ist klein, damit der Trainer sich Ihre eigene Konfiguration ansehen kann.</p>
|
||||
<p class="bc-note">Ein Platz ist <strong>jederzeit übertragbar</strong> und kann <strong>bis zwei Wochen vor</strong> der ersten Sitzung am 21. Januar 2027 storniert werden.</p>
|
||||
<p class="bc-lead">
|
||||
Umfasst das komplette Programm: zehn Live-Sessions, ein Jahr gehostetes Odoo Community Edition, die Alumni-Community und die Expertenprüfung.
|
||||
</p>
|
||||
<div class="bc-action">
|
||||
<form method="post" action="https://20.odoo4projects.com/shop/post_basket" target="_blank" class="plausible-event-name=Buy">
|
||||
<input type="hidden" name="basket" value='[{"product_id":12,"qty":1}]' />
|
||||
<input type="hidden" name="basket" value='[{"product_id":17,"qty":1,"info":"Q1 2027, English"}]' />
|
||||
<input type="hidden" name="clear" value="1" />
|
||||
<button type="submit" class="btn btn-brass btn-lg">Für 465.00 USD kaufen</button>
|
||||
<button type="submit" class="btn btn-brass btn-lg">Für 595.00 USD kaufen</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Waiting list: for a visitor who cannot attend the January cohort.
|
||||
Secondary to the Buy button above. Same CRM webhook and same payload shape as the
|
||||
site's footer contact form (area:'message'); the copy lives here, in the page's
|
||||
language, and /static/bootcamp-waitlist.js carries no strings. -->
|
||||
<div class="bc-waitlist">
|
||||
<h3 class="bc-wl-title">Andere Zeitzone oder andere Sprache?</h3>
|
||||
<p class="bc-wl-lead">Der Januar-Kurs läuft donnerstags 18:00–19:00 US-Zentralzeit, auf Englisch. Wenn Sie eine andere Uhrzeit oder eine andere Sprache brauchen, tragen Sie sich in die Warteliste ein. Wir schreiben Ihnen, sobald ein passender Kurs startet.</p>
|
||||
<form id="bcWaitlistForm" class="bc-wl-form" action="https://n8n.odoo4projects.com/webhook/ODOO4projects/crm" method="post" enctype="text/plain">
|
||||
<label class="bc-sr-only" for="bcWaitlistEmail">Ihre E-Mail-Adresse</label>
|
||||
<input type="email" id="bcWaitlistEmail" name="email" placeholder="Ihre E-Mail-Adresse" required />
|
||||
<label class="bc-sr-only" for="bcWaitlistComment">Welche Zeitzone oder Sprache Sie brauchen</label>
|
||||
<textarea id="bcWaitlistComment" name="comment" rows="2" placeholder="Welche Zeitzone oder Sprache brauchen Sie?" required></textarea>
|
||||
<button type="submit" class="btn bc-wl-btn" data-sending="Wird eingetragen…">In die Warteliste eintragen</button>
|
||||
</form>
|
||||
<p class="bc-wl-success" id="bcWaitlistSuccess" role="status" style="display:none">Sie stehen auf der Warteliste. Wir schreiben Ihnen, sobald ein Kurs in Ihrer Zeitzone oder Sprache angekündigt wird.</p>
|
||||
<p class="bc-wl-error" id="bcWaitlistError" role="alert" style="display:none">Etwas ist schiefgelaufen. Bitte versuchen Sie es erneut.</p>
|
||||
</div>
|
||||
<ul class="bc-related">
|
||||
<li><a href="/de/support.html#contact">Fragen vor der Buchung? Sprechen Sie zuerst mit uns</a></li>
|
||||
<li><a href="/de/odoo-hosting-2027.html">Wie das Hosting nach Ihrem Jahr bei uns aussieht</a></li>
|
||||
@@ -828,6 +967,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<!-- Footer (copied from index.html; icon-font glyphs replaced with inline SVG so the page
|
||||
carries no external icon dependency) -->
|
||||
@@ -835,7 +975,7 @@
|
||||
<div class="container">
|
||||
<div class="fgrid">
|
||||
<div>
|
||||
<h4>Nützliche Links</h4>
|
||||
<h3>Nützliche Links</h3>
|
||||
<ul>
|
||||
<li><a href="/de/odoo-erp-automation-bootcamp/">ODOO Bootcamp</a></li>
|
||||
<li><a href="/de/odoo-hosting-2027.html">ODOO Hosting in 2027</a></li>
|
||||
@@ -844,7 +984,7 @@
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<h4>Partner</h4>
|
||||
<h3>Partner</h3>
|
||||
<div class="partner-list">
|
||||
<a href="https://oc-guy.com" target="_blank" rel="noopener" class="partner-item"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#c9a227" stroke-width="2" aria-hidden="true"><path d="M20 7h-9M14 17H5M20 7l-4-4M20 7l-4 4M4 17l4 4M4 17l4-4" /></svg> OC Guy</a>
|
||||
<a href="https://oca-py.org" target="_blank" rel="noopener" class="partner-item"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#c9a227" stroke-width="2" aria-hidden="true"><path d="M20 7h-9M14 17H5M20 7l-4-4M20 7l-4 4M4 17l4 4M4 17l4-4" /></svg> OCA Paraguay</a>
|
||||
@@ -852,17 +992,17 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="fcol-desc">
|
||||
<h4>Über uns</h4>
|
||||
<h3>Über uns</h3>
|
||||
<p>Wir bauen clevere, skalierbare Geschäftslösungen: die <strong>Stabilität</strong> von Odoo ERP trifft auf die <strong>Kraft</strong> von KI-Agenten und N8N-Automatisierungen.</p>
|
||||
<p>Von schlanken Vertriebsprozessen bis zum globalen Rollout: Unser <strong>schlanker, umweltfreundlicher</strong> Ansatz senkt Kosten, steigert die Effizienz und lässt Unternehmen ohne Grenzen wachsen.</p>
|
||||
</div>
|
||||
<div>
|
||||
<h4>Folgen Sie uns</h4>
|
||||
<h3>Folgen Sie uns</h3>
|
||||
<div class="social">
|
||||
<a href="https://www.linkedin.com/company/odoo4projects/" target="_blank" aria-label="LinkedIn"><svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M4.98 3.5a2.5 2.5 0 1 1 0 5 2.5 2.5 0 0 1 0-5zM3 9h4v12H3zM9 9h3.8v1.7h.05c.53-.95 1.83-1.95 3.76-1.95 4.02 0 4.76 2.5 4.76 5.75V21h-4v-5.5c0-1.31-.02-3-1.9-3-1.9 0-2.19 1.42-2.19 2.9V21H9z" /></svg></a>
|
||||
<a href="https://www.youtube.com/@ODOO4projects" target="_blank" aria-label="YouTube"><svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M23 12s0-3.2-.41-4.73a2.5 2.5 0 0 0-1.76-1.77C19.3 5.1 12 5.1 12 5.1s-7.3 0-8.83.4A2.5 2.5 0 0 0 1.4 7.27C1 8.8 1 12 1 12s0 3.2.41 4.73a2.5 2.5 0 0 0 1.76 1.77c1.53.4 8.83.4 8.83.4s7.3 0 8.83-.4a2.5 2.5 0 0 0 1.76-1.77C23 15.2 23 12 23 12zM9.75 15.02V8.98L15.5 12z" /></svg></a>
|
||||
</div>
|
||||
<form class="footer-contact-form" id="footerContactForm" action="mailto:hello@odoo4projects.com" method="post" enctype="text/plain">
|
||||
<form class="footer-contact-form" id="footerContactForm" action="https://n8n.odoo4projects.com/webhook/ODOO4projects/crm" method="post" enctype="text/plain" data-ok="Danke! Wir melden uns in Kürze bei Ihnen." data-err="Etwas ist schiefgelaufen. Bitte versuchen Sie es erneut.">
|
||||
<label class="bc-sr-only" for="footerContactEmail">Ihre E-Mail-Adresse</label>
|
||||
<input type="email" id="footerContactEmail" name="email" placeholder="Ihre E-Mail-Adresse" required />
|
||||
<div class="footer-textarea-wrap">
|
||||
@@ -872,12 +1012,15 @@
|
||||
</div>
|
||||
<div class="footer-form-status" id="footerFormStatus" role="status"></div>
|
||||
</form>
|
||||
<noscript><p class="bc-noscript">JavaScript ist deaktiviert. Schreiben Sie uns direkt an <a href="mailto:support@odoo4projects.com">support@odoo4projects.com</a>.</p></noscript>
|
||||
</div>
|
||||
</div>
|
||||
<div class="fcopy">Copyright © ODOO4projects LLC</div>
|
||||
</div>
|
||||
</footer>
|
||||
<script defer src="/static/buy.js"></script>
|
||||
<script defer src="/static/lang.js"></script>
|
||||
<script defer src="/static/buy.js?v=31314de600"></script>
|
||||
<script defer src="/static/bootcamp-availability.js?v=94dfd8cd7c"></script>
|
||||
<script defer src="/static/bootcamp-waitlist.js?v=6c2e2fbd33"></script>
|
||||
<script defer src="/static/lang.js?v=3ec49a7e62"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -31,8 +31,7 @@
|
||||
onload="this.media='all'"
|
||||
/>
|
||||
<noscript><link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Marcellus&display=swap" /></noscript>
|
||||
<link rel="stylesheet" href="/style.css" media="print" onload="this.media='all'" />
|
||||
<noscript><link rel="stylesheet" href="/style.css" /></noscript>
|
||||
<link rel="stylesheet" href="/style.css?v=061517baef" />
|
||||
<style>
|
||||
/* ---- Odoo Hosting 2027 landing page (page-scoped; additive) ---- */
|
||||
.cmp-sr-only {
|
||||
@@ -479,7 +478,7 @@
|
||||
{
|
||||
"@type": "Product",
|
||||
"name": "Verwaltetes Odoo Community Hosting",
|
||||
"description": "Verwaltetes Odoo Community Hosting: Server, Backups, SSL-Zertifikat und Versions-Upgrades laufen für Sie zu einem Jahrespreis, ohne Lizenz pro Benutzer bei Odoo Community.",
|
||||
"description": "Verwaltetes Odoo Community Hosting: Server, Backups und SSL-Zertifikat laufen für Sie, und Ihre Odoo-Installation bleibt innerhalb ihrer Version aktuell, zu einem Jahrespreis, ohne Lizenz pro Benutzer bei Odoo Community. Ein Wechsel auf eine neue Hauptversion ist eine Migration und wird separat angeboten.",
|
||||
"brand": { "@id": "https://odoo4projects.com/#organization" },
|
||||
"offers": [
|
||||
{
|
||||
@@ -560,7 +559,7 @@
|
||||
window.plausible=window.plausible||function(){(plausible.q=plausible.q||[]).push(arguments)},plausible.init=plausible.init||function(i){plausible.o=i||{}};
|
||||
plausible.init()
|
||||
</script>
|
||||
<script defer src="/static/lang.js"></script>
|
||||
<script defer src="/static/lang.js?v=3ec49a7e62"></script>
|
||||
</head>
|
||||
<body>
|
||||
<!-- Page-scoped top bar: the site has no global header, so the page carries its own way home. -->
|
||||
@@ -576,14 +575,15 @@
|
||||
</div>
|
||||
|
||||
<!-- Hero -->
|
||||
<main>
|
||||
<section class="hero cmp-hero">
|
||||
<div class="container">
|
||||
<p class="hero-kicker">Vollständig verwaltetes Odoo Community Hosting</p>
|
||||
<h1>Odoo Hosting 2027: verwaltetes Odoo Community, <span>in etwa fünf Minuten einsatzbereit</span></h1>
|
||||
<p class="hero-sub">
|
||||
Ein verwaltetes Odoo Community System: Ihr Lager, Ihre Aufträge, Ihre Rechnungen und Ihre Kunden in
|
||||
einem System. Wir betreiben Server, Backups, Zertifikat und Upgrades. Sie pflegen Ihre Daten ein und
|
||||
kümmern sich ums Geschäft.
|
||||
einem System. Wir betreiben Server, Backups, Zertifikat und die Updates innerhalb Ihrer Odoo-Version.
|
||||
Sie pflegen Ihre Daten ein und kümmern sich ums Geschäft.
|
||||
</p>
|
||||
<div class="hero-stats">
|
||||
<span class="hero-stat"><i aria-hidden="true">✓</i> Etwa 5 Minuten bis zum laufenden System</span>
|
||||
@@ -606,7 +606,9 @@
|
||||
<p class="cmp-lead">
|
||||
Sie sollten nicht die IT-Abteilung werden müssen, um zu wissen, was in Ihrem Lager liegt. Ein
|
||||
Geschäftssystem abonniert man, man baut es nicht. Server, Patches, Backups, Zertifikat,
|
||||
Versions-Upgrade: das ist dauerhaft unsere Aufgabe, und kein Abend, den Sie Ihrer Firma zurückgeben.
|
||||
die Updates innerhalb Ihrer Odoo-Version: das ist dauerhaft unsere Aufgabe, und kein Abend,
|
||||
den Sie Ihrer Firma zurückgeben. Die Datenbank auf eine neue Hauptversion zu bringen, ist eine
|
||||
Migration, die wir separat anbieten.
|
||||
</p>
|
||||
<p class="cmp-lead">
|
||||
Deshalb bringen wir Ihr Lager, Ihre Aufträge, Ihre Rechnungen und die offenen Forderungen an einen
|
||||
@@ -703,7 +705,7 @@
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Versions-Upgrades</th>
|
||||
<td class="is-us">Ein Support-Ticket. Wir migrieren die Versionen 17, 18 und 19.</td>
|
||||
<td class="is-us">Updates und Upgrades innerhalb Ihrer Version sind inklusive. Ein Wechsel auf eine neue Hauptversion ist eine Migration und wird separat angeboten.</td>
|
||||
<td>Sie machen es selbst, nach einem vollständigen Backup.</td>
|
||||
<td>Push in den Branch; scheitert ein Modul-Update, setzt Odoo den Server zurück und rollt die Datenbank zurück.</td>
|
||||
<td>Automatisch und verpflichtend nach Odoos Zeitplan.</td>
|
||||
@@ -979,6 +981,7 @@
|
||||
|
||||
<!-- Footer (copied from index.html; icon-font glyphs replaced with inline SVG so the page
|
||||
carries no external icon dependency) -->
|
||||
</main>
|
||||
<footer class="footer">
|
||||
<div class="container">
|
||||
<div class="fgrid">
|
||||
@@ -1010,7 +1013,7 @@
|
||||
<a href="https://www.linkedin.com/company/odoo4projects/" target="_blank" aria-label="LinkedIn"><svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M4.98 3.5a2.5 2.5 0 1 1 0 5 2.5 2.5 0 0 1 0-5zM3 9h4v12H3zM9 9h3.8v1.7h.05c.53-.95 1.83-1.95 3.76-1.95 4.02 0 4.76 2.5 4.76 5.75V21h-4v-5.5c0-1.31-.02-3-1.9-3-1.9 0-2.19 1.42-2.19 2.9V21H9z" /></svg></a>
|
||||
<a href="https://www.youtube.com/@ODOO4projects" target="_blank" aria-label="YouTube"><svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M23 12s0-3.2-.41-4.73a2.5 2.5 0 0 0-1.76-1.77C19.3 5.1 12 5.1 12 5.1s-7.3 0-8.83.4A2.5 2.5 0 0 0 1.4 7.27C1 8.8 1 12 1 12s0 3.2.41 4.73a2.5 2.5 0 0 0 1.76 1.77c1.53.4 8.83.4 8.83.4s7.3 0 8.83-.4a2.5 2.5 0 0 0 1.76-1.77C23 15.2 23 12 23 12zM9.75 15.02V8.98L15.5 12z" /></svg></a>
|
||||
</div>
|
||||
<form class="footer-contact-form" id="footerContactForm" action="mailto:hello@odoo4projects.com" method="post" enctype="text/plain">
|
||||
<form class="footer-contact-form" id="footerContactForm" action="mailto:support@odoo4projects.com" method="post" enctype="text/plain">
|
||||
<label class="cmp-sr-only" for="footerContactEmail">Ihre E-Mail-Adresse</label>
|
||||
<input type="email" id="footerContactEmail" name="email" placeholder="Ihre E-Mail-Adresse" required />
|
||||
<div class="footer-textarea-wrap">
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
rel="stylesheet"
|
||||
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css"
|
||||
/>
|
||||
<link rel="stylesheet" href="/style.css" />
|
||||
<link rel="stylesheet" href="/style.css?v=061517baef" />
|
||||
<style>
|
||||
/* Language selector — the page's top row: the back-link stays left, the selector travels right.
|
||||
Cream canvas, so lang.js's own dark-brass active colour is the correct one (no override). */
|
||||
@@ -274,9 +274,10 @@
|
||||
window.plausible=window.plausible||function(){(plausible.q=plausible.q||[]).push(arguments)},plausible.init=plausible.init||function(i){plausible.o=i||{}};
|
||||
plausible.init()
|
||||
</script>
|
||||
<script defer src="/static/lang.js"></script>
|
||||
<script defer src="/static/lang.js?v=3ec49a7e62"></script>
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<div class="container">
|
||||
<div class="blog-header">
|
||||
<a href="/de/">← Zurück zur Startseite</a>
|
||||
@@ -291,7 +292,9 @@
|
||||
<p>
|
||||
Für die schnellste Hilfe lesen Sie bitte zuerst unsere <strong>FAQ</strong> — unser Support-Bot
|
||||
hilft bei schnellen Lösungen wie <strong>git pull</strong> oder Neustarts — oder senden Sie
|
||||
einfach ein <strong>Ticket</strong> an unser Support-Team.
|
||||
einfach ein <strong>Ticket</strong> an unser Support-Team. Dringende Themen kann der
|
||||
<strong>KI-Agent</strong> übernehmen, den wir anbieten, und E-Mail-Support antwortet
|
||||
innerhalb von <strong>24 Stunden</strong>.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
@@ -582,6 +585,7 @@
|
||||
});
|
||||
</script>
|
||||
|
||||
<script defer src="/static/agent-odoo4projects/agent.js"></script>
|
||||
</main>
|
||||
<script defer src="/static/agent-odoo4projects/agent.js?v=c3bb0cee2b"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -128,7 +128,9 @@
|
||||
.hero-stats{display:flex;gap:1.5rem;margin-bottom:2rem;flex-wrap:wrap}.hero-stat{display:flex;align-items:center;gap:.4rem;color:rgba(255,255,255,.65);font-size:.85rem}.hero-stat i{color:var(--brass);font-size:.75rem}
|
||||
.hero-actions{display:flex;gap:1rem;flex-wrap:wrap;margin-bottom:2rem}
|
||||
.hero-notice{position:absolute;top:1.25rem;right:1.25rem;z-index:3;display:flex;align-items:center;gap:.55rem;max-width:min(360px,calc(100% - 2rem));padding:.6rem .85rem .6rem 1rem;background:#f5c542;color:var(--midnight);border:1px solid rgba(13,27,46,.14);border-radius:var(--radius);box-shadow:0 8px 24px rgba(13,27,46,.3);font-family:Inter,sans-serif;font-size:.82rem;line-height:1.4;animation:heroNoticeIn .4s cubic-bezier(.16,1,.3,1)}
|
||||
.hero-notice[hidden]{display:none}
|
||||
/* `.hero-notice[hidden]{display:none}` used to live here; the site-wide `[hidden]`
|
||||
rule in /style.css hides it (`.hero-notice` sets `display:flex`, which outranked
|
||||
the UA rule). */
|
||||
.hero-notice i{color:var(--midnight);font-size:.9rem;flex-shrink:0}
|
||||
.hero-notice-text{flex:1;min-width:0;overflow-wrap:anywhere}
|
||||
.hero-notice-close{flex-shrink:0;display:flex;align-items:center;justify-content:center;width:1.35rem;height:1.35rem;padding:0;border:0;border-radius:50%;background:rgba(13,27,46,.12);color:var(--midnight);font-size:1rem;line-height:1;cursor:pointer;transition:background .2s}
|
||||
@@ -220,15 +222,14 @@
|
||||
#customQuoteModal .biz-success p{color:var(--midnight)}
|
||||
@media(max-width:600px){.oca-table td,.oca-table th{padding:.5rem .65rem;font-size:.8rem}.ow-panel{left:5%;right:5%}.ow-close{top:.5rem;right:.5rem;font-size:1.6rem}}
|
||||
</style>
|
||||
<link rel="stylesheet" href="/style.css" media="print" onload="this.media='all'" />
|
||||
<noscript><link rel="stylesheet" href="/style.css" /></noscript>
|
||||
<link rel="stylesheet" href="/style.css?v=061517baef" />
|
||||
<!-- Privacy-friendly analytics by Plausible -->
|
||||
<script async src="https://plausible.office.odoo4projects.com/js/pa-NhA7Rf-79VopXFQXhSS4e.js"></script>
|
||||
<script>
|
||||
window.plausible=window.plausible||function(){(plausible.q=plausible.q||[]).push(arguments)},plausible.init=plausible.init||function(i){plausible.o=i||{}};
|
||||
plausible.init()
|
||||
</script>
|
||||
<script defer src="/static/lang.js"></script>
|
||||
<script defer src="/static/lang.js?v=3ec49a7e62"></script>
|
||||
</head>
|
||||
<body>
|
||||
<!--
|
||||
@@ -240,6 +241,7 @@
|
||||
FINISH: unreviewed and undocumented is unfinished; this build ends with the finish review, the verdict, and DESIGN.md.
|
||||
-->
|
||||
<!-- Hero -->
|
||||
<main>
|
||||
<section class="hero">
|
||||
<div class="hero-notice" id="heroConfirmNotice" role="status" aria-live="polite" hidden>
|
||||
<i class="fas fa-check-circle" aria-hidden="true"></i>
|
||||
@@ -496,7 +498,7 @@
|
||||
<h2 class="ow-title" id="orderPlanTitle">On the Rise</h2>
|
||||
<p class="ow-price">
|
||||
<span class="ow-price-amount" id="orderPlanPrice">$ 395.00</span> <span id="orderPlanPeriod">/ año</span>
|
||||
<span class="ow-price-tax" id="orderPlanTax">+ impuestos</span>
|
||||
<span class="ow-price-tax" id="orderPlanTax">impuestos incluidos</span>
|
||||
</p>
|
||||
<p class="ow-coupon-msg" id="orderCouponMsg">¿Dudas antes de comprar? Pregunte al asistente en esta página.</p>
|
||||
<p class="ow-yearly-save" id="orderYearlySave">Facturación anual. Sin cuota de alta.</p>
|
||||
@@ -625,6 +627,7 @@
|
||||
</section>
|
||||
|
||||
<!-- Footer -->
|
||||
</main>
|
||||
<footer class="footer">
|
||||
<div class="container">
|
||||
<div class="fgrid">
|
||||
@@ -671,7 +674,7 @@
|
||||
</footer>
|
||||
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.5/gsap.min.js"></script>
|
||||
<script src="/script.js"></script>
|
||||
<script defer src="/static/agent-odoo4projects/agent.js"></script>
|
||||
<script src="/script.js?v=d2046e3469"></script>
|
||||
<script defer src="/static/agent-odoo4projects/agent.js?v=c3bb0cee2b"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -38,8 +38,7 @@
|
||||
onload="this.media='all'"
|
||||
/>
|
||||
<noscript><link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Marcellus&display=swap" /></noscript>
|
||||
<link rel="stylesheet" href="/style.css" media="print" onload="this.media='all'" />
|
||||
<noscript><link rel="stylesheet" href="/style.css" /></noscript>
|
||||
<link rel="stylesheet" href="/style.css?v=061517baef" />
|
||||
<style>
|
||||
/* ---- Odoo Community Upgrade page (page-scoped; additive) ---- */
|
||||
.oc-sr-only {
|
||||
@@ -480,7 +479,7 @@
|
||||
window.plausible=window.plausible||function(){(plausible.q=plausible.q||[]).push(arguments)},plausible.init=plausible.init||function(i){plausible.o=i||{}};
|
||||
plausible.init()
|
||||
</script>
|
||||
<script defer src="/static/lang.js"></script>
|
||||
<script defer src="/static/lang.js?v=3ec49a7e62"></script>
|
||||
</head>
|
||||
<body>
|
||||
<!-- Page-scoped top bar: the site has no global header, so the page carries its own way home. -->
|
||||
@@ -496,6 +495,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Hero -->
|
||||
<main>
|
||||
<section class="hero oc-hero">
|
||||
<div class="hero-bg">
|
||||
<img
|
||||
@@ -837,6 +837,7 @@ sudo -u postgres pg_restore -d <DB_NEW> /path/backup/<DB>.dump
|
||||
|
||||
<!-- Footer (copied from index.html; icon-font glyphs replaced with inline SVG so the page
|
||||
carries no external icon dependency) -->
|
||||
</main>
|
||||
<footer class="footer">
|
||||
<div class="container">
|
||||
<div class="fgrid">
|
||||
@@ -868,7 +869,7 @@ sudo -u postgres pg_restore -d <DB_NEW> /path/backup/<DB>.dump
|
||||
<a href="https://www.linkedin.com/company/odoo4projects/" target="_blank" aria-label="LinkedIn"><svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M4.98 3.5a2.5 2.5 0 1 1 0 5 2.5 2.5 0 0 1 0-5zM3 9h4v12H3zM9 9h3.8v1.7h.05c.53-.95 1.83-1.95 3.76-1.95 4.02 0 4.76 2.5 4.76 5.75V21h-4v-5.5c0-1.31-.02-3-1.9-3-1.9 0-2.19 1.42-2.19 2.9V21H9z" /></svg></a>
|
||||
<a href="https://www.youtube.com/@ODOO4projects" target="_blank" aria-label="YouTube"><svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M23 12s0-3.2-.41-4.73a2.5 2.5 0 0 0-1.76-1.77C19.3 5.1 12 5.1 12 5.1s-7.3 0-8.83.4A2.5 2.5 0 0 0 1.4 7.27C1 8.8 1 12 1 12s0 3.2.41 4.73a2.5 2.5 0 0 0 1.76 1.77c1.53.4 8.83.4 8.83.4s7.3 0 8.83-.4a2.5 2.5 0 0 0 1.76-1.77C23 15.2 23 12 23 12zM9.75 15.02V8.98L15.5 12z" /></svg></a>
|
||||
</div>
|
||||
<form class="footer-contact-form" id="footerContactForm" action="mailto:hello@odoo4projects.com" method="post" enctype="text/plain">
|
||||
<form class="footer-contact-form" id="footerContactForm" action="mailto:support@odoo4projects.com" method="post" enctype="text/plain">
|
||||
<label class="oc-sr-only" for="footerContactEmail">Su dirección de correo</label>
|
||||
<input type="email" id="footerContactEmail" name="email" placeholder="Su dirección de correo" required />
|
||||
<div class="footer-textarea-wrap">
|
||||
|
||||
@@ -31,8 +31,9 @@
|
||||
onload="this.media='all'"
|
||||
/>
|
||||
<noscript><link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Marcellus&display=swap" /></noscript>
|
||||
<link rel="stylesheet" href="/style.css" media="print" onload="this.media='all'" />
|
||||
<noscript><link rel="stylesheet" href="/style.css" /></noscript>
|
||||
<link rel="preload" as="image" href="/images/hero/bootcamp-hero-mobile-900.webp" media="(max-width: 640px)" fetchpriority="high" />
|
||||
<link rel="preload" as="image" href="/images/hero/bootcamp-hero-desktop-1920.webp" media="(min-width: 641px)" fetchpriority="high" />
|
||||
<link rel="stylesheet" href="/style.css?v=061517baef" />
|
||||
<style>
|
||||
/* ---- ODOO ERP Automation Bootcamp landing page (page-scoped; additive) ---- */
|
||||
.bc-sr-only {
|
||||
@@ -192,6 +193,14 @@
|
||||
form[action$="/shop/post_basket"] .btn {
|
||||
line-height: inherit;
|
||||
}
|
||||
/* At 0 seats /static/bootcamp-availability.js disables these Buy buttons; make the
|
||||
disabled state read as disabled — quiet dimming in the page's own CI, never an
|
||||
alert banner. */
|
||||
form[action$="/shop/post_basket"] .btn:disabled,
|
||||
form[action$="/shop/post_basket"] .btn[aria-disabled="true"] {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* Grids — auto-fit so no breakpoint is needed */
|
||||
.bc-list {
|
||||
@@ -223,7 +232,7 @@
|
||||
border-radius: var(--radius-lg);
|
||||
box-shadow: var(--shadow-rest);
|
||||
padding: 1.4rem 1.4rem 1.4rem 3.4rem;
|
||||
transition: box-shadow 0.25s, transform 0.25s;
|
||||
transition: transform 0.25s; /* box-shadow kept, not transitioned (non-composited) */
|
||||
}
|
||||
.bc-lessons li:hover {
|
||||
box-shadow: var(--shadow-hover);
|
||||
@@ -313,7 +322,7 @@
|
||||
.bc-schedule dt {
|
||||
font-family: Marcellus, Georgia, serif;
|
||||
font-size: 0.92rem;
|
||||
color: var(--brass-deep);
|
||||
color: #876c1e; /* 4.68:1 on --ivory (was --brass-deep 4.49:1) */
|
||||
}
|
||||
.bc-schedule dd {
|
||||
margin: 0;
|
||||
@@ -452,6 +461,109 @@
|
||||
.btn-brass:focus-visible {
|
||||
outline-color: #fff;
|
||||
}
|
||||
/* Waiting list — for someone who cannot attend the January cohort.
|
||||
Deliberately quieter than the Buy button: outline button, smaller type, no brass fill. */
|
||||
.bc-waitlist {
|
||||
/* Component-scoped error colour: the site has no shared token for it. */
|
||||
--wl-error: #b02a37;
|
||||
max-width: 720px;
|
||||
margin: 2.5rem auto 0;
|
||||
padding: 1.5rem;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--radius);
|
||||
background: var(--ivory);
|
||||
text-align: left;
|
||||
}
|
||||
.bc-wl-title {
|
||||
margin: 0 0 0.5rem;
|
||||
font-size: 1.1rem;
|
||||
color: var(--ink);
|
||||
}
|
||||
.bc-wl-lead {
|
||||
margin: 0 0 1.1rem;
|
||||
font-size: 0.88rem;
|
||||
line-height: 1.6;
|
||||
color: rgba(23, 28, 38, 0.78); /* the page's own .bc-note value, on the same cream */
|
||||
}
|
||||
.bc-wl-form input,
|
||||
.bc-wl-form textarea {
|
||||
display: block;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
margin-bottom: 0.6rem;
|
||||
padding: 0.6rem 0.75rem;
|
||||
font-family: Inter, sans-serif;
|
||||
font-size: 0.9rem;
|
||||
color: var(--ink);
|
||||
background: #fff;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--radius);
|
||||
}
|
||||
.bc-wl-form textarea {
|
||||
min-height: 3.4rem;
|
||||
resize: vertical;
|
||||
}
|
||||
.bc-wl-btn {
|
||||
padding: 0.6rem 1.5rem;
|
||||
font-size: 0.8rem;
|
||||
background: transparent;
|
||||
color: var(--ink);
|
||||
border-color: rgba(23, 28, 38, 0.35);
|
||||
}
|
||||
.bc-wl-btn:hover {
|
||||
border-color: var(--brass);
|
||||
background: rgba(201, 162, 39, 0.12);
|
||||
transform: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
.bc-wl-btn:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: default;
|
||||
}
|
||||
.bc-wl-success {
|
||||
margin: 0;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
color: var(--success);
|
||||
}
|
||||
.bc-wl-error {
|
||||
margin: 0;
|
||||
font-size: 0.9rem;
|
||||
color: var(--wl-error);
|
||||
}
|
||||
|
||||
/* --- 2026-11-02 PageSpeed pass (page-scoped, additive) ---------------------
|
||||
1. The footer <h4>s became <h3>s so the heading order descends after the last
|
||||
section h2 (a11y "heading-order"); these rules keep their look identical to
|
||||
the shared .footer h4 in /style.css.
|
||||
2. A link inside an on-dark note is the lighter brass (7.15:1 on midnight,
|
||||
A11y "color-contrast") and underlined, so it is distinguishable without
|
||||
colour (A11y "link-in-text-block"). */
|
||||
.footer h3 {
|
||||
font-family: Marcellus, Georgia, serif;
|
||||
color: #fff;
|
||||
margin-bottom: 0.8rem;
|
||||
font-size: 1rem;
|
||||
letter-spacing: 0.03em;
|
||||
}
|
||||
.bc-note.on-dark a {
|
||||
color: var(--brass);
|
||||
text-decoration: underline;
|
||||
}
|
||||
/* the footer copyright line at .35 alpha was 3.2:1 on midnight */
|
||||
.footer .fcopy {
|
||||
color: rgba(255, 255, 255, 0.5); /* 5.17:1 */
|
||||
}
|
||||
/* no-JS footer fallback (rendered only when JavaScript is off) */
|
||||
.footer .bc-noscript {
|
||||
margin-top: 0.6rem;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
.footer .bc-noscript a {
|
||||
color: var(--brass);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
</style>
|
||||
<script type="application/ld+json">
|
||||
{
|
||||
@@ -501,7 +613,7 @@
|
||||
},
|
||||
"offers": {
|
||||
"@type": "Offer",
|
||||
"price": "465.00",
|
||||
"price": "595.00",
|
||||
"priceCurrency": "USD",
|
||||
"category": "Paid",
|
||||
"url": "https://odoo4projects.com/odoo-erp-automation-bootcamp/"
|
||||
@@ -576,6 +688,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Hero -->
|
||||
<main>
|
||||
<section class="hero bc-hero">
|
||||
<div class="container">
|
||||
<p class="hero-kicker">Q1 2027 · 10 SESIONES EN DIRECTO · OLIVER ARNOLD</p>
|
||||
@@ -591,14 +704,21 @@
|
||||
</div>
|
||||
<div class="hero-actions">
|
||||
<form method="post" action="https://20.odoo4projects.com/shop/post_basket" target="_blank" class="plausible-event-name=Buy">
|
||||
<input type="hidden" name="basket" value='[{"product_id":12,"qty":1}]' />
|
||||
<input type="hidden" name="basket" value='[{"product_id":17,"qty":1,"info":"Q1 2027, English"}]' />
|
||||
<input type="hidden" name="clear" value="1" />
|
||||
<button type="submit" class="btn btn-brass btn-lg">Comprar por 465.00 USD</button>
|
||||
<button type="submit" class="btn btn-brass btn-lg">Comprar por 595.00 USD</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Live remaining-seat count, immediately below the hero. One seat element only on
|
||||
the page; /static/bootcamp-availability.js fills it from the availability webhook
|
||||
and leaves it empty+hidden if that fetch fails. -->
|
||||
<div class="container">
|
||||
<p class="bc-note bc-seats" data-bc-seats hidden></p>
|
||||
</div>
|
||||
|
||||
<!-- From zero to a running ERP -->
|
||||
<section class="section-light">
|
||||
<div class="container">
|
||||
@@ -612,9 +732,9 @@
|
||||
<p class="bc-subline">Tres meses. Diez horas de clases. Un sistema que dirige su empresa.</p>
|
||||
<div class="bc-action">
|
||||
<form method="post" action="https://20.odoo4projects.com/shop/post_basket" target="_blank" class="plausible-event-name=Buy">
|
||||
<input type="hidden" name="basket" value='[{"product_id":12,"qty":1}]' />
|
||||
<input type="hidden" name="basket" value='[{"product_id":17,"qty":1,"info":"Q1 2027, English"}]' />
|
||||
<input type="hidden" name="clear" value="1" />
|
||||
<button type="submit" class="btn btn-brass btn-lg">Comprar por 465.00 USD</button>
|
||||
<button type="submit" class="btn btn-brass btn-lg">Comprar por 595.00 USD</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
@@ -769,18 +889,37 @@
|
||||
<section class="section-light">
|
||||
<div class="container">
|
||||
<h2 class="section-title">Precio y reserva</h2>
|
||||
<p class="bc-price">El programa cuesta USD 465.00 por las diez sesiones.</p>
|
||||
<p class="bc-price">El programa cuesta USD 595.00 por las diez sesiones.</p>
|
||||
<p class="bc-note"><strong>Limitado a 6 participantes</strong> — el grupo es reducido, para que el instructor pueda trabajar sobre su propia configuración.</p>
|
||||
<p class="bc-note">Una plaza se puede <strong>transferir en cualquier momento</strong> y <strong>cancelar hasta dos semanas antes</strong> de la primera sesión, el 21 de enero de 2027.</p>
|
||||
<p class="bc-lead">
|
||||
Cubre el programa completo: diez sesiones en directo, un año de Odoo Community Edition alojado, la comunidad de antiguos alumnos y la revisión experta.
|
||||
</p>
|
||||
<div class="bc-action">
|
||||
<form method="post" action="https://20.odoo4projects.com/shop/post_basket" target="_blank" class="plausible-event-name=Buy">
|
||||
<input type="hidden" name="basket" value='[{"product_id":12,"qty":1}]' />
|
||||
<input type="hidden" name="basket" value='[{"product_id":17,"qty":1,"info":"Q1 2027, English"}]' />
|
||||
<input type="hidden" name="clear" value="1" />
|
||||
<button type="submit" class="btn btn-brass btn-lg">Comprar por 465.00 USD</button>
|
||||
<button type="submit" class="btn btn-brass btn-lg">Comprar por 595.00 USD</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Waiting list: for a visitor who cannot attend the January cohort.
|
||||
Secondary to the Buy button above. Same CRM webhook and same payload shape as the
|
||||
site's footer contact form (area:'message'); the copy lives here, in the page's
|
||||
language, and /static/bootcamp-waitlist.js carries no strings. -->
|
||||
<div class="bc-waitlist">
|
||||
<h3 class="bc-wl-title">¿Otra zona horaria u otro idioma?</h3>
|
||||
<p class="bc-wl-lead">El grupo de enero se reúne los jueves, 18:00–19:00, hora del centro de EE. UU., en inglés. Si necesita otro horario u otro idioma, apúntese a la lista de espera y le escribiremos cuando se abra un grupo que le encaje.</p>
|
||||
<form id="bcWaitlistForm" class="bc-wl-form" action="https://n8n.odoo4projects.com/webhook/ODOO4projects/crm" method="post" enctype="text/plain">
|
||||
<label class="bc-sr-only" for="bcWaitlistEmail">Su dirección de correo electrónico</label>
|
||||
<input type="email" id="bcWaitlistEmail" name="email" placeholder="Su dirección de correo electrónico" required />
|
||||
<label class="bc-sr-only" for="bcWaitlistComment">Qué zona horaria o idioma necesita</label>
|
||||
<textarea id="bcWaitlistComment" name="comment" rows="2" placeholder="¿Qué zona horaria o idioma necesita?" required></textarea>
|
||||
<button type="submit" class="btn bc-wl-btn" data-sending="Apuntando…">Apuntarme a la lista de espera</button>
|
||||
</form>
|
||||
<p class="bc-wl-success" id="bcWaitlistSuccess" role="status" style="display:none">Está en la lista de espera. Le escribiremos en cuanto se anuncie un grupo en su zona horaria o idioma.</p>
|
||||
<p class="bc-wl-error" id="bcWaitlistError" role="alert" style="display:none">Algo ha fallado. Inténtelo de nuevo.</p>
|
||||
</div>
|
||||
<ul class="bc-related">
|
||||
<li><a href="/es/support.html#contact">¿Preguntas antes de reservar? Hable antes con nosotros</a></li>
|
||||
<li><a href="/es/odoo-hosting-2027.html">Cómo es el hosting después de su año con nosotros</a></li>
|
||||
@@ -828,6 +967,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<!-- Footer (copied from index.html; icon-font glyphs replaced with inline SVG so the page
|
||||
carries no external icon dependency) -->
|
||||
@@ -835,7 +975,7 @@
|
||||
<div class="container">
|
||||
<div class="fgrid">
|
||||
<div>
|
||||
<h4>Enlaces útiles</h4>
|
||||
<h3>Enlaces útiles</h3>
|
||||
<ul>
|
||||
<li><a href="/es/odoo-erp-automation-bootcamp/">ODOO Bootcamp</a></li>
|
||||
<li><a href="/es/odoo-hosting-2027.html">ODOO Hosting en 2027</a></li>
|
||||
@@ -844,7 +984,7 @@
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<h4>Socios</h4>
|
||||
<h3>Socios</h3>
|
||||
<div class="partner-list">
|
||||
<a href="https://oc-guy.com" target="_blank" rel="noopener" class="partner-item"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#c9a227" stroke-width="2" aria-hidden="true"><path d="M20 7h-9M14 17H5M20 7l-4-4M20 7l-4 4M4 17l4 4M4 17l4-4" /></svg> OC Guy</a>
|
||||
<a href="https://oca-py.org" target="_blank" rel="noopener" class="partner-item"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#c9a227" stroke-width="2" aria-hidden="true"><path d="M20 7h-9M14 17H5M20 7l-4-4M20 7l-4 4M4 17l4 4M4 17l4-4" /></svg> OCA Paraguay</a>
|
||||
@@ -852,17 +992,17 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="fcol-desc">
|
||||
<h4>Sobre nosotros</h4>
|
||||
<h3>Sobre nosotros</h3>
|
||||
<p>Creamos soluciones de negocio inteligentes y escalables combinando la <strong>estabilidad</strong> de Odoo ERP con la <strong>potencia</strong> de los agentes de IA y las automatizaciones N8N.</p>
|
||||
<p>Desde unas ventas ágiles hasta despliegues globales, nuestro <strong>lean y ecológico</strong> enfoque ayuda a las empresas a reducir costes, ganar eficiencia y crecer sin límites.</p>
|
||||
</div>
|
||||
<div>
|
||||
<h4>Síganos</h4>
|
||||
<h3>Síganos</h3>
|
||||
<div class="social">
|
||||
<a href="https://www.linkedin.com/company/odoo4projects/" target="_blank" aria-label="LinkedIn"><svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M4.98 3.5a2.5 2.5 0 1 1 0 5 2.5 2.5 0 0 1 0-5zM3 9h4v12H3zM9 9h3.8v1.7h.05c.53-.95 1.83-1.95 3.76-1.95 4.02 0 4.76 2.5 4.76 5.75V21h-4v-5.5c0-1.31-.02-3-1.9-3-1.9 0-2.19 1.42-2.19 2.9V21H9z" /></svg></a>
|
||||
<a href="https://www.youtube.com/@ODOO4projects" target="_blank" aria-label="YouTube"><svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M23 12s0-3.2-.41-4.73a2.5 2.5 0 0 0-1.76-1.77C19.3 5.1 12 5.1 12 5.1s-7.3 0-8.83.4A2.5 2.5 0 0 0 1.4 7.27C1 8.8 1 12 1 12s0 3.2.41 4.73a2.5 2.5 0 0 0 1.76 1.77c1.53.4 8.83.4 8.83.4s7.3 0 8.83-.4a2.5 2.5 0 0 0 1.76-1.77C23 15.2 23 12 23 12zM9.75 15.02V8.98L15.5 12z" /></svg></a>
|
||||
</div>
|
||||
<form class="footer-contact-form" id="footerContactForm" action="mailto:hello@odoo4projects.com" method="post" enctype="text/plain">
|
||||
<form class="footer-contact-form" id="footerContactForm" action="https://n8n.odoo4projects.com/webhook/ODOO4projects/crm" method="post" enctype="text/plain" data-ok="¡Gracias! Le responderemos en breve." data-err="Algo ha fallado. Inténtelo de nuevo.">
|
||||
<label class="bc-sr-only" for="footerContactEmail">Su dirección de correo</label>
|
||||
<input type="email" id="footerContactEmail" name="email" placeholder="Su dirección de correo" required />
|
||||
<div class="footer-textarea-wrap">
|
||||
@@ -872,12 +1012,15 @@
|
||||
</div>
|
||||
<div class="footer-form-status" id="footerFormStatus" role="status"></div>
|
||||
</form>
|
||||
<noscript><p class="bc-noscript">JavaScript está desactivado. Escríbanos directamente a <a href="mailto:support@odoo4projects.com">support@odoo4projects.com</a>.</p></noscript>
|
||||
</div>
|
||||
</div>
|
||||
<div class="fcopy">Copyright © ODOO4projects LLC</div>
|
||||
</div>
|
||||
</footer>
|
||||
<script defer src="/static/buy.js"></script>
|
||||
<script defer src="/static/lang.js"></script>
|
||||
<script defer src="/static/buy.js?v=31314de600"></script>
|
||||
<script defer src="/static/bootcamp-availability.js?v=94dfd8cd7c"></script>
|
||||
<script defer src="/static/bootcamp-waitlist.js?v=6c2e2fbd33"></script>
|
||||
<script defer src="/static/lang.js?v=3ec49a7e62"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -31,8 +31,7 @@
|
||||
onload="this.media='all'"
|
||||
/>
|
||||
<noscript><link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Marcellus&display=swap" /></noscript>
|
||||
<link rel="stylesheet" href="/style.css" media="print" onload="this.media='all'" />
|
||||
<noscript><link rel="stylesheet" href="/style.css" /></noscript>
|
||||
<link rel="stylesheet" href="/style.css?v=061517baef" />
|
||||
<style>
|
||||
/* ---- Odoo Hosting 2027 landing page (page-scoped; additive) ---- */
|
||||
.cmp-sr-only {
|
||||
@@ -479,7 +478,7 @@
|
||||
{
|
||||
"@type": "Product",
|
||||
"name": "Hosting gestionado de Odoo Community",
|
||||
"description": "Hosting gestionado de Odoo Community: el servidor, las copias de seguridad, el certificado SSL y las actualizaciones de versión corren por nuestra cuenta a un precio anual, sin licencia por usuario en Odoo Community.",
|
||||
"description": "Hosting gestionado de Odoo Community: el servidor, las copias de seguridad y el certificado SSL corren por nuestra cuenta, y mantenemos su Odoo al día dentro de su versión, a un precio anual, sin licencia por usuario en Odoo Community. Cambiar a una nueva versión mayor es una migración y se presupuesta por separado.",
|
||||
"brand": { "@id": "https://odoo4projects.com/#organization" },
|
||||
"offers": [
|
||||
{
|
||||
@@ -560,7 +559,7 @@
|
||||
window.plausible=window.plausible||function(){(plausible.q=plausible.q||[]).push(arguments)},plausible.init=plausible.init||function(i){plausible.o=i||{}};
|
||||
plausible.init()
|
||||
</script>
|
||||
<script defer src="/static/lang.js"></script>
|
||||
<script defer src="/static/lang.js?v=3ec49a7e62"></script>
|
||||
</head>
|
||||
<body>
|
||||
<!-- Page-scoped top bar: the site has no global header, so the page carries its own way home. -->
|
||||
@@ -576,6 +575,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Hero -->
|
||||
<main>
|
||||
<section class="hero cmp-hero">
|
||||
<div class="container">
|
||||
<p class="hero-kicker">Hosting de Odoo Community totalmente gestionado</p>
|
||||
@@ -583,7 +583,7 @@
|
||||
<p class="hero-sub">
|
||||
Un sistema Odoo Community gestionado: su inventario, sus pedidos, sus facturas y sus clientes en un
|
||||
solo sistema. Nosotros nos ocupamos del servidor, las copias de seguridad, el certificado y las
|
||||
actualizaciones. Usted introduce sus datos y se dedica a su negocio.
|
||||
actualizaciones dentro de su versión de Odoo. Usted introduce sus datos y se dedica a su negocio.
|
||||
</p>
|
||||
<div class="hero-stats">
|
||||
<span class="hero-stat"><i aria-hidden="true">✓</i> Unos 5 minutos hasta tener el sistema funcionando</span>
|
||||
@@ -606,8 +606,10 @@
|
||||
<p class="cmp-lead">
|
||||
No debería tener que convertirse en el departamento de informática para saber qué hay en su
|
||||
inventario. Un sistema de gestión se contrata como un servicio, no se construye como un proyecto. El
|
||||
servidor, los parches, las copias de seguridad, el certificado, la actualización de versión: eso es
|
||||
servidor, los parches, las copias de seguridad, el certificado, las actualizaciones dentro de su versión de Odoo: eso es
|
||||
nuestro trabajo, de forma permanente, y no una tarde que usted le devuelve a su empresa.
|
||||
Llevar la base de datos a una nueva versión mayor es una migración que
|
||||
presupuestamos por separado.
|
||||
</p>
|
||||
<p class="cmp-lead">
|
||||
Por eso reunimos su inventario, sus pedidos, sus facturas y quién le debe dinero en un solo lugar, y
|
||||
@@ -704,7 +706,7 @@
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Actualizaciones de versión</th>
|
||||
<td class="is-us">Un ticket de soporte. Migramos las versiones 17, 18 y 19.</td>
|
||||
<td class="is-us">Las actualizaciones y las mejoras dentro de su versión están incluidas. Cambiar a una nueva versión mayor es una migración y se presupuesta por separado.</td>
|
||||
<td>Lo hace usted, después de una copia de seguridad completa.</td>
|
||||
<td>Suba la rama; si falla la actualización de un módulo, Odoo revierte el servidor y restaura la base de datos.</td>
|
||||
<td>Automáticas y obligatorias según el calendario de Odoo.</td>
|
||||
@@ -978,6 +980,7 @@
|
||||
|
||||
<!-- Footer (copied from index.html; icon-font glyphs replaced with inline SVG so the page
|
||||
carries no external icon dependency) -->
|
||||
</main>
|
||||
<footer class="footer">
|
||||
<div class="container">
|
||||
<div class="fgrid">
|
||||
@@ -1009,7 +1012,7 @@
|
||||
<a href="https://www.linkedin.com/company/odoo4projects/" target="_blank" aria-label="LinkedIn"><svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M4.98 3.5a2.5 2.5 0 1 1 0 5 2.5 2.5 0 0 1 0-5zM3 9h4v12H3zM9 9h3.8v1.7h.05c.53-.95 1.83-1.95 3.76-1.95 4.02 0 4.76 2.5 4.76 5.75V21h-4v-5.5c0-1.31-.02-3-1.9-3-1.9 0-2.19 1.42-2.19 2.9V21H9z" /></svg></a>
|
||||
<a href="https://www.youtube.com/@ODOO4projects" target="_blank" aria-label="YouTube"><svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M23 12s0-3.2-.41-4.73a2.5 2.5 0 0 0-1.76-1.77C19.3 5.1 12 5.1 12 5.1s-7.3 0-8.83.4A2.5 2.5 0 0 0 1.4 7.27C1 8.8 1 12 1 12s0 3.2.41 4.73a2.5 2.5 0 0 0 1.76 1.77c1.53.4 8.83.4 8.83.4s7.3 0 8.83-.4a2.5 2.5 0 0 0 1.76-1.77C23 15.2 23 12 23 12zM9.75 15.02V8.98L15.5 12z" /></svg></a>
|
||||
</div>
|
||||
<form class="footer-contact-form" id="footerContactForm" action="mailto:hello@odoo4projects.com" method="post" enctype="text/plain">
|
||||
<form class="footer-contact-form" id="footerContactForm" action="mailto:support@odoo4projects.com" method="post" enctype="text/plain">
|
||||
<label class="cmp-sr-only" for="footerContactEmail">Su dirección de correo</label>
|
||||
<input type="email" id="footerContactEmail" name="email" placeholder="Su dirección de correo" required />
|
||||
<div class="footer-textarea-wrap">
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
rel="stylesheet"
|
||||
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css"
|
||||
/>
|
||||
<link rel="stylesheet" href="/style.css" />
|
||||
<link rel="stylesheet" href="/style.css?v=061517baef" />
|
||||
<style>
|
||||
/* Language selector — the page's top row: the back-link stays left, the selector travels right.
|
||||
Cream canvas, so lang.js's own dark-brass active colour is the correct one (no override). */
|
||||
@@ -274,9 +274,10 @@
|
||||
window.plausible=window.plausible||function(){(plausible.q=plausible.q||[]).push(arguments)},plausible.init=plausible.init||function(i){plausible.o=i||{}};
|
||||
plausible.init()
|
||||
</script>
|
||||
<script defer src="/static/lang.js"></script>
|
||||
<script defer src="/static/lang.js?v=3ec49a7e62"></script>
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<div class="container">
|
||||
<div class="blog-header">
|
||||
<a href="/es/">← Volver al inicio</a>
|
||||
@@ -291,7 +292,9 @@
|
||||
<p>
|
||||
Para una ayuda más rápida, lea primero nuestras <strong>preguntas frecuentes</strong>: nuestro
|
||||
bot de soporte le ayuda con soluciones rápidas como <strong>git pull</strong> o reinicios — o
|
||||
envíe simplemente un <strong>ticket</strong> a nuestro equipo de soporte.
|
||||
envíe simplemente un <strong>ticket</strong> a nuestro equipo de soporte. Los temas urgentes los puede
|
||||
atender el <strong>agente de IA</strong> que ofrecemos, y el soporte por correo responde
|
||||
en un plazo de <strong>24 horas</strong>.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
@@ -582,6 +585,7 @@
|
||||
});
|
||||
</script>
|
||||
|
||||
<script defer src="/static/agent-odoo4projects/agent.js"></script>
|
||||
</main>
|
||||
<script defer src="/static/agent-odoo4projects/agent.js?v=c3bb0cee2b"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,486 @@
|
||||
<!doctype html>
|
||||
<html lang="en-US">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Freelancer signup feedback — ODOO4projects</title>
|
||||
<meta name="description" content="Tell us how the ODOO4projects freelancer signup went — every answer helps us improve the process before the directory opens." />
|
||||
|
||||
<!-- Private utility page. It is reached ONLY by the link we send, never by search.
|
||||
noindex+nofollow here, absent from sitemap.xml and llms.txt, and linked from nowhere
|
||||
on the site. See [[draft-page-convention]] for the same five rule. -->
|
||||
<meta name="robots" content="noindex, nofollow" />
|
||||
<!-- Nothing in this page's address is a credential, but the page is private and the
|
||||
feedback is a person's words: keep the URL out of any third party's logs. -->
|
||||
<meta name="referrer" content="no-referrer" />
|
||||
<link rel="canonical" href="https://odoo4projects.com/feedback/" />
|
||||
<meta property="og:url" content="https://odoo4projects.com/feedback/" />
|
||||
<meta property="og:title" content="Freelancer signup feedback — ODOO4projects" />
|
||||
<meta property="og:description" content="Tell us how the ODOO4projects freelancer signup went — every answer helps us improve the process." />
|
||||
<meta property="og:type" content="website" />
|
||||
<link rel="icon" href="/images/favicon.ico" />
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Marcellus&display=swap"
|
||||
media="print"
|
||||
onload="this.media='all'"
|
||||
/>
|
||||
<noscript><link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Marcellus&display=swap" /></noscript>
|
||||
<link rel="stylesheet" href="/style.css?v=061517baef" />
|
||||
|
||||
<style>
|
||||
/* Utility page, same shell as /freelancer-edit/ and the top bar of
|
||||
/odoo-community-developers/: brand line, one card, the site tokens (:root in style.css)
|
||||
and the CI palette only. No hero, no footer, no site chrome beyond the brand line. */
|
||||
* { box-sizing: border-box; }
|
||||
body {
|
||||
margin: 0;
|
||||
background: var(--cream);
|
||||
color: var(--ink);
|
||||
font-family: Inter, "Segoe UI", sans-serif;
|
||||
line-height: 1.6;
|
||||
}
|
||||
.wrap { width: 86%; max-width: 1200px; margin: 0 auto; }
|
||||
.topbar { background: var(--midnight); border-bottom: 1px solid var(--line-brass); padding: 0.85rem 0; }
|
||||
.brand {
|
||||
font-family: Marcellus, Georgia, serif;
|
||||
font-size: 1.15rem;
|
||||
letter-spacing: 0.04em;
|
||||
color: #fff;
|
||||
text-decoration: none;
|
||||
}
|
||||
.brand span { color: var(--brass); }
|
||||
main { padding: 2.5rem 0 4rem; }
|
||||
.card-wrap { max-width: 760px; margin: 0 auto; }
|
||||
h1 {
|
||||
font-family: Marcellus, Georgia, serif;
|
||||
font-weight: 400;
|
||||
font-size: clamp(1.5rem, 4vw, 2.2rem);
|
||||
letter-spacing: 0.02em;
|
||||
margin: 0 0 0.4rem;
|
||||
}
|
||||
.lede { color: var(--ink-soft); margin: 0 0 1rem; }
|
||||
.panel {
|
||||
background: var(--ivory);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: 1.5rem;
|
||||
box-shadow: var(--shadow-rest);
|
||||
}
|
||||
.panel + .panel { margin-top: 1.25rem; }
|
||||
h2 {
|
||||
font-family: Marcellus, Georgia, serif;
|
||||
font-weight: 400;
|
||||
font-size: 1.15rem;
|
||||
letter-spacing: 0.02em;
|
||||
margin: 0 0 0.6rem;
|
||||
}
|
||||
.state { margin: 0; font-weight: 500; }
|
||||
.state-warn, .state-err { color: var(--brass-deep); }
|
||||
.state-ok { color: var(--success); }
|
||||
.detail { margin: 0.5rem 0 0; font-size: 0.9rem; color: var(--ink-soft); }
|
||||
|
||||
/* ---- Forms ---- */
|
||||
.field { margin: 0 0 1.1rem; }
|
||||
.field > label { display: block; font-weight: 600; font-size: 0.9rem; margin: 0 0 0.35rem; }
|
||||
input[type="text"], input[type="email"], textarea, select {
|
||||
width: 100%;
|
||||
font: inherit;
|
||||
padding: 0.6rem 0.7rem;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--radius);
|
||||
background: #fff;
|
||||
color: var(--ink);
|
||||
}
|
||||
textarea { resize: vertical; }
|
||||
input:focus-visible, textarea:focus-visible, select:focus-visible, button:focus-visible,
|
||||
input[type="radio"]:focus-visible, input[type="checkbox"]:focus-visible {
|
||||
outline: 2px solid var(--brass);
|
||||
outline-offset: 1px;
|
||||
}
|
||||
.hint { font-size: 0.82rem; color: var(--ink-soft); margin: 0.35rem 0 0; }
|
||||
fieldset { border: 1px solid var(--line); border-radius: var(--radius); padding: 0.9rem 1rem 0.4rem; margin: 0 0 1.1rem; }
|
||||
legend { font-weight: 600; font-size: 0.9rem; padding: 0 0.35rem; }
|
||||
.choice { display: flex; align-items: flex-start; gap: 0.55rem; margin: 0 0 0.5rem; font-size: 0.9rem; }
|
||||
.choice input { margin-top: 0.28rem; flex: 0 0 auto; }
|
||||
.field[data-bad="1"] input, .field[data-bad="1"] textarea, .field[data-bad="1"] select,
|
||||
fieldset[data-bad="1"] { border-color: var(--brass-deep); }
|
||||
.err { font-size: 0.82rem; color: var(--brass-deep); font-weight: 600; margin: 0.35rem 0 0; }
|
||||
|
||||
.btn {
|
||||
display: inline-block;
|
||||
font-family: Marcellus, Georgia, serif;
|
||||
font-size: 0.9rem;
|
||||
letter-spacing: 0.04em;
|
||||
border: 0;
|
||||
border-radius: var(--radius-pill);
|
||||
background: var(--brass);
|
||||
color: var(--midnight);
|
||||
padding: 0.7rem 1.5rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
.btn:hover { background: var(--brass-bright); }
|
||||
.btn:focus-visible { outline: 2px solid var(--midnight); outline-offset: 2px; }
|
||||
.btn[disabled] { opacity: 0.6; cursor: default; }
|
||||
.status { margin: 0.9rem 0 0; font-weight: 500; min-height: 1.2em; }
|
||||
.status.state-warn, .status.state-err { color: var(--brass-deep); }
|
||||
.status.state-ok { color: var(--success); }
|
||||
|
||||
/* ---- Progress bar (two steps) ---- */
|
||||
.progress { margin: 0 0 1.15rem; }
|
||||
.steps-bar {
|
||||
list-style: none;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 0.3rem 0.6rem;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
.steps-bar .step { display: flex; align-items: center; gap: 0.4rem; color: var(--ink-soft); }
|
||||
.steps-bar .step + .step::before { content: "·"; margin-right: 0.6rem; color: var(--line-brass); }
|
||||
.step-mark {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 1.55rem;
|
||||
height: 1.55rem;
|
||||
border-radius: 999px;
|
||||
border: 1px solid var(--line);
|
||||
background: #fff;
|
||||
font-size: 0.8rem;
|
||||
line-height: 1;
|
||||
}
|
||||
/* The current step is the brass pill and the bolder label, and it is said out loud
|
||||
("(current step)", .vh below) — weight and a word, so it is never colour alone. */
|
||||
.steps-bar .step-current { color: var(--ink); font-weight: 600; }
|
||||
.steps-bar .step-current .step-mark { background: var(--brass); border-color: var(--brass); color: var(--midnight); }
|
||||
.link-signup { color: var(--brass-deep); font-weight: 600; }
|
||||
/* Visually hidden, still read aloud. */
|
||||
.vh {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
margin: -1px;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
clip: rect(0 0 0 0);
|
||||
white-space: nowrap;
|
||||
border: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
<!-- The one wire name and shape this page uses, in ONE place. POST only — the page reads
|
||||
nothing on load. SHAPE (match this in the n8n workflow):
|
||||
|
||||
POST submitUrl { "form": "feedback", <one key per question, the answer> }
|
||||
-> any 2xx = receipt shown; a non-2xx is reported as a failure, never as success.
|
||||
|
||||
The body carries the answers and nothing else: no name, no email, no identifier of any
|
||||
kind. An unexpected shape is named, never guessed. -->
|
||||
<script>
|
||||
window.O4F = {
|
||||
// The one route that receives the feedback form.
|
||||
submitUrl: "https://n8n.odoo4projects.com/webhook/feedback"
|
||||
};
|
||||
</script>
|
||||
|
||||
<!-- Privacy-friendly analytics by Plausible (verbatim; the page carries no query). -->
|
||||
<script async src="https://plausible.office.odoo4projects.com/js/pa-NhA7Rf-79VopXFQXhSS4e.js"></script>
|
||||
<script>
|
||||
window.plausible=window.plausible||function(){(plausible.q=plausible.q||[]).push(arguments)},plausible.init=plausible.init||function(i){plausible.o=i||{}};
|
||||
plausible.init()
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<header class="topbar">
|
||||
<div class="wrap">
|
||||
<a class="brand" href="https://odoo4projects.com/">ODOO<span>4</span>projects</a>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<div class="wrap">
|
||||
<div class="card-wrap">
|
||||
<h1>Freelancer signup feedback</h1>
|
||||
<p class="lede">
|
||||
We opened a free freelancer portal on ODOO4projects.com. Sign up there, then tell us how
|
||||
it went — every answer helps us improve the process.
|
||||
</p>
|
||||
<!-- The portal link, visible next to the intro and not buried in step copy. It opens in
|
||||
a new window and says so. It is a plain link, not the page's primary action —
|
||||
the feedback form still is. -->
|
||||
<p class="detail">
|
||||
The directory opens on <strong>1 December</strong>. Signing up is free for freelancers
|
||||
and clients, and the feedback you give here is what we fix before then.
|
||||
<a
|
||||
class="link-signup"
|
||||
href="https://odoo4projects.com/odoo-community-developers/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>Sign up on the freelancer portal (opens in a new window)</a>.
|
||||
</p>
|
||||
|
||||
<!-- Filled by the script at the foot of the page. Empty on purpose: with JavaScript off
|
||||
there is no form to render, so an empty container plus the noscript note below is
|
||||
the honest picture. -->
|
||||
<div id="app"></div>
|
||||
|
||||
<noscript>
|
||||
<div class="panel">
|
||||
<p class="state state-warn">This page needs JavaScript to show the feedback form.</p>
|
||||
<p class="detail">
|
||||
Turn JavaScript on in your browser and open this page again — or reply to the message
|
||||
that brought you here.
|
||||
</p>
|
||||
</div>
|
||||
</noscript>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
var C = window.O4F;
|
||||
var app = document.getElementById("app");
|
||||
// The freelancer portal — the directory page, at its TOP (its own introduction), not the
|
||||
// directory's apply anchor: that anchor drops the visitor at the application form,
|
||||
// mid-page, past the page's own intro. There is no separate login or account page, so this
|
||||
// is the one target. The static .link-signup link in the markup above is the ONE source of
|
||||
// this URL; the script-written link in step 1 reads it back, so the two can never disagree.
|
||||
var PORTAL_URL = document.querySelector(".link-signup").href;
|
||||
|
||||
function el(tag, cls, text) {
|
||||
var n = document.createElement(tag);
|
||||
if (cls) n.className = cls;
|
||||
if (text != null) n.textContent = text; // textContent only: a reply is never markup
|
||||
return n;
|
||||
}
|
||||
function show(nodes) {
|
||||
while (app.firstChild) app.removeChild(app.firstChild);
|
||||
for (var i = 0; i < nodes.length; i++) app.appendChild(nodes[i]);
|
||||
return nodes;
|
||||
}
|
||||
function status(text, kind) {
|
||||
var p = el("p", "status" + (kind ? " state-" + kind : ""), text);
|
||||
p.setAttribute("role", "status");
|
||||
p.setAttribute("aria-live", "polite");
|
||||
return p;
|
||||
}
|
||||
function fetchJson(url, opts) {
|
||||
return fetch(url, opts).then(function (r) {
|
||||
return r.text().then(function (t) {
|
||||
var body = null;
|
||||
try { body = JSON.parse(t); } catch (e) { body = null; }
|
||||
return { ok: r.ok, status: r.status, body: body };
|
||||
});
|
||||
});
|
||||
}
|
||||
function msgOf(body) {
|
||||
if (!body || typeof body !== "object") return "";
|
||||
var m = body.message || body.error || body.detail;
|
||||
return typeof m === "string" ? m : "";
|
||||
}
|
||||
// A link to the freelancer portal. Opens in a new window and says so, so a screen-reader
|
||||
// user is not surprised by a new tab.
|
||||
function portalLink() {
|
||||
var p = el("p", "detail");
|
||||
var a = el("a", "link-signup");
|
||||
a.href = PORTAL_URL;
|
||||
a.target = "_blank";
|
||||
a.rel = "noopener noreferrer";
|
||||
a.textContent = "Sign up on the freelancer portal (opens in a new window)";
|
||||
p.appendChild(a);
|
||||
return p;
|
||||
}
|
||||
|
||||
// ---- Progress bar (two steps) ---------------------------------------------------------
|
||||
// Step 1 is the signup on the portal, step 2 is this form. The page has no honest way to
|
||||
// know the visitor has used the portal link, so step 1 stays current and nothing is
|
||||
// guessed: the number, the bolder label and the spoken "(current step)" say which one is
|
||||
// current, so it is never colour alone.
|
||||
var STEP_LABELS = ["Sign up", "Give feedback"];
|
||||
function progressBar(current) {
|
||||
var nav = el("nav", "progress");
|
||||
nav.setAttribute("aria-label", "Progress");
|
||||
var ol = el("ol", "steps-bar");
|
||||
STEP_LABELS.forEach(function (label, i) {
|
||||
var n = i + 1;
|
||||
var li = el("li", "step");
|
||||
var mark = el("span", "step-mark");
|
||||
mark.setAttribute("aria-hidden", "true");
|
||||
mark.textContent = String(n);
|
||||
li.appendChild(mark);
|
||||
li.appendChild(el("span", "step-label", label));
|
||||
if (n === current) {
|
||||
li.className = "step step-current";
|
||||
li.setAttribute("aria-current", "step");
|
||||
li.appendChild(el("span", "vh", " (current step)"));
|
||||
} else {
|
||||
li.className = "step step-next";
|
||||
}
|
||||
ol.appendChild(li);
|
||||
});
|
||||
nav.appendChild(ol);
|
||||
return nav;
|
||||
}
|
||||
|
||||
// ---- Step 1 — the portal link, again, in the step's own copy -------------------------
|
||||
function signupPanel() {
|
||||
var box = el("div", "panel");
|
||||
box.appendChild(el("h2", "", "Step 1 — sign up on the freelancer portal"));
|
||||
box.appendChild(el("p", "detail",
|
||||
"Sign up on the ODOO4projects freelancer portal and add your profile. The directory opens on 1 December."));
|
||||
box.appendChild(portalLink());
|
||||
return box;
|
||||
}
|
||||
|
||||
// ---- The feedback form ----------------------------------------------------------------
|
||||
// The n8n form's questions, with the copy corrected. The identity block (name, email,
|
||||
// anonymous tick) and the publish-consent question are gone — do not bring them back.
|
||||
// None of these questions is compulsory: the form submits with any subset answered, or none.
|
||||
// The error nodes (.err) and their CSS are kept, but nothing sets them any more.
|
||||
var FIELDS = [
|
||||
{ id: "finalized", label: "Did you finalize the signup?", type: "select", options: [["yes", "Yes"], ["no", "No"]] },
|
||||
{ id: "difficulty", label: "How hard was the signup?", type: "select", options: [["easy", "Easy"], ["ok", "OK"], ["hard", "Hard"]] },
|
||||
{ id: "signup_feedback", label: "What is your feedback on the signup?", type: "textarea" },
|
||||
{ id: "understood_scale", label: "Did you understand how we ask you to rate your skills? (the 15-point scale)", type: "select", options: [["yes", "Yes"], ["no", "No"]] },
|
||||
{ id: "scale_feedback", label: "What is your feedback on the 15-point skill scale?", type: "textarea" },
|
||||
{ id: "almost_stopped", label: "What almost stopped you from signing up?", type: "textarea", hint: "If nothing did, say so." },
|
||||
{ id: "broke_or_confused", label: "Did anything break or confuse you?", type: "textarea" },
|
||||
{ id: "design_shows_work", label: "Do you think our profile design shows what you do well?", type: "select", options: [["yes", "Yes"], ["so-so", "So-so"], ["no", "No"]] },
|
||||
{ id: "design_feedback", label: "What can we improve about the profile design?", type: "textarea" },
|
||||
{ id: "will_stay", label: "Will you stay on the platform?", type: "select", options: [["yes", "Yes"], ["no", "No"]] },
|
||||
{ id: "improve_offer", label: "How can we improve what we offer you?", type: "textarea" },
|
||||
{ id: "recommend", label: "Would you recommend this platform to a freelance colleague?", type: "select", options: [["yes", "Yes, good idea"], ["maybe", "Maybe"], ["no", "Preferably not"]] },
|
||||
{ id: "expect_next", label: "What do you expect to happen next — and how soon?", type: "textarea" }
|
||||
];
|
||||
|
||||
function fieldBlock(spec) {
|
||||
var f = el("div", "field");
|
||||
f.setAttribute("data-field", spec.id);
|
||||
if (spec.type === "radio") {
|
||||
var fs = el("fieldset");
|
||||
fs.setAttribute("data-field", spec.id);
|
||||
var lg = el("legend", "", spec.label);
|
||||
fs.appendChild(lg);
|
||||
spec.options.forEach(function (pair) {
|
||||
var lab = el("label", "choice");
|
||||
var r = el("input");
|
||||
r.type = "radio";
|
||||
r.name = spec.id;
|
||||
r.value = pair[0];
|
||||
r.id = spec.id + "-" + pair[0];
|
||||
lab.appendChild(r);
|
||||
lab.appendChild(el("span", "", pair[1]));
|
||||
fs.appendChild(lab);
|
||||
});
|
||||
var er = el("p", "err", "");
|
||||
er.hidden = true;
|
||||
fs.appendChild(er);
|
||||
return fs;
|
||||
}
|
||||
var lb = el("label", "", spec.label);
|
||||
lb.setAttribute("for", "fb-" + spec.id);
|
||||
var ctl;
|
||||
if (spec.type === "select") {
|
||||
ctl = el("select");
|
||||
ctl.appendChild(new Option("— choose —", ""));
|
||||
spec.options.forEach(function (pair) { ctl.appendChild(new Option(pair[1], pair[0])); });
|
||||
} else if (spec.type === "textarea") {
|
||||
ctl = el("textarea");
|
||||
ctl.rows = 4;
|
||||
} else {
|
||||
ctl = el("input");
|
||||
ctl.type = "text";
|
||||
}
|
||||
ctl.id = "fb-" + spec.id;
|
||||
ctl.name = spec.id;
|
||||
f.appendChild(lb);
|
||||
f.appendChild(ctl);
|
||||
if (spec.hint) f.appendChild(el("p", "hint", spec.hint));
|
||||
var e = el("p", "err", "");
|
||||
e.hidden = true;
|
||||
f.appendChild(e);
|
||||
return f;
|
||||
}
|
||||
|
||||
function feedbackPanel() {
|
||||
var box = el("div", "panel");
|
||||
box.appendChild(el("h2", "", "How did it go?"));
|
||||
box.appendChild(el("p", "detail", "Answer in your own words. Your answers come to us — nothing here is published."));
|
||||
var form = el("form");
|
||||
form.setAttribute("novalidate", "novalidate");
|
||||
|
||||
FIELDS.forEach(function (spec) { form.appendChild(fieldBlock(spec)); });
|
||||
|
||||
var line = status("", "");
|
||||
var btn = el("button", "btn", "Send feedback");
|
||||
btn.type = "submit";
|
||||
form.appendChild(btn);
|
||||
form.appendChild(line);
|
||||
|
||||
form.addEventListener("submit", function (ev) {
|
||||
ev.preventDefault();
|
||||
|
||||
// Nothing here is compulsory, so there is nothing to validate and nothing to announce.
|
||||
// The .err nodes and their CSS are kept in place in case a question is ever made
|
||||
// compulsory again, but nothing here sets them: an empty form submits like any other.
|
||||
|
||||
// Build the body. Only the questions that were answered, and nothing else — no
|
||||
// name, no email, no identifier, and no empty string for a question left blank.
|
||||
var body = { form: "feedback" };
|
||||
FIELDS.forEach(function (spec) {
|
||||
var v = "";
|
||||
if (spec.type === "radio") {
|
||||
var checked = form.querySelector('input[name="' + spec.id + '"]:checked');
|
||||
v = checked ? checked.value : "";
|
||||
} else {
|
||||
var ctl = form.querySelector("#fb-" + spec.id);
|
||||
v = ctl ? ctl.value.trim() : "";
|
||||
}
|
||||
if (v) body[spec.id] = v;
|
||||
});
|
||||
|
||||
btn.disabled = true;
|
||||
line.className = "status";
|
||||
line.textContent = "Sending…";
|
||||
fetchJson(C.submitUrl, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(body)
|
||||
}).then(function (res) {
|
||||
if (!res.ok) {
|
||||
btn.disabled = false;
|
||||
line.className = "status state-warn";
|
||||
line.textContent = "We could not send your feedback — the service answered " + res.status +
|
||||
(msgOf(res.body) ? " — " + msgOf(res.body) : "") + ". Your answers are still here; try again.";
|
||||
return;
|
||||
}
|
||||
line.className = "status state-ok";
|
||||
line.textContent = "Thank you — your feedback reached us.";
|
||||
btn.disabled = true;
|
||||
form.querySelectorAll("input, textarea, select, button").forEach(function (n) {
|
||||
if (n !== btn) n.disabled = true;
|
||||
});
|
||||
}).catch(function () {
|
||||
btn.disabled = false;
|
||||
line.className = "status state-warn";
|
||||
line.textContent = "We could not reach the service. Your answers are still here; try again.";
|
||||
});
|
||||
});
|
||||
|
||||
box.appendChild(form);
|
||||
return box;
|
||||
}
|
||||
|
||||
// ---- Render ---------------------------------------------------------------------------
|
||||
// One state, always: the form. Nothing is read on load, so there is no fetch here.
|
||||
show([progressBar(1), signupPanel(), feedbackPanel()]);
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,468 @@
|
||||
<!doctype html>
|
||||
<html lang="en-US">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Answer a question — ODOO4projects</title>
|
||||
<meta name="description" content="Private page: an Odoo freelancer answers a client's question." />
|
||||
|
||||
<!-- Private utility page. It is reached ONLY by a link we email, never by search.
|
||||
noindex+nofollow here, absent from sitemap.xml and llms.txt, and linked from nowhere
|
||||
on the site. See [[draft-page-convention]] for the same five rule. -->
|
||||
<meta name="robots" content="noindex, nofollow" />
|
||||
<!-- While the link is open the address carries a credential. No claim-by-subdomain of the
|
||||
hash travels to a third party (Google Fonts, and every other cross-origin request). -->
|
||||
<meta name="referrer" content="no-referrer" />
|
||||
<link rel="canonical" href="https://odoo4projects.com/freelancer-answer/" />
|
||||
<link rel="icon" href="/images/favicon.ico" />
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Marcellus&display=swap"
|
||||
media="print"
|
||||
onload="this.media='all'"
|
||||
/>
|
||||
<noscript><link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Marcellus&display=swap" /></noscript>
|
||||
<link rel="stylesheet" href="/style.css?v=061517baef" />
|
||||
|
||||
<style>
|
||||
/* Utility page. Deliberately small: no hero, no footer, no site chrome beyond a
|
||||
brand line. Reuses the site tokens (:root in style.css) and the CI palette only. */
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
body {
|
||||
margin: 0;
|
||||
background: var(--cream);
|
||||
color: var(--ink);
|
||||
font-family: Inter, "Segoe UI", sans-serif;
|
||||
line-height: 1.6;
|
||||
}
|
||||
.wrap {
|
||||
width: 86%;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.topbar {
|
||||
background: var(--midnight);
|
||||
border-bottom: 1px solid var(--line-brass);
|
||||
padding: 0.85rem 0;
|
||||
}
|
||||
.brand {
|
||||
font-family: Marcellus, Georgia, serif;
|
||||
font-size: 1.15rem;
|
||||
letter-spacing: 0.04em;
|
||||
color: #fff;
|
||||
text-decoration: none;
|
||||
}
|
||||
.brand span {
|
||||
color: var(--brass);
|
||||
}
|
||||
main {
|
||||
padding: 2.5rem 0 4rem;
|
||||
}
|
||||
.card-wrap {
|
||||
max-width: 720px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
h1 {
|
||||
font-family: Marcellus, Georgia, serif;
|
||||
font-weight: 400;
|
||||
font-size: clamp(1.5rem, 4vw, 2.2rem);
|
||||
letter-spacing: 0.02em;
|
||||
margin: 0 0 0.4rem;
|
||||
}
|
||||
.lede {
|
||||
color: var(--ink-soft);
|
||||
margin: 0 0 1.75rem;
|
||||
}
|
||||
.panel {
|
||||
background: var(--ivory);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: 1.5rem;
|
||||
box-shadow: var(--shadow-rest);
|
||||
}
|
||||
.state {
|
||||
margin: 0;
|
||||
font-weight: 500;
|
||||
}
|
||||
.state-warn,
|
||||
.state-err {
|
||||
color: var(--brass-deep);
|
||||
}
|
||||
.state-ok {
|
||||
color: var(--success);
|
||||
}
|
||||
.detail {
|
||||
margin: 0.5rem 0 0;
|
||||
font-size: 0.9rem;
|
||||
color: var(--ink-soft);
|
||||
}
|
||||
.qcard {
|
||||
background: var(--midnight);
|
||||
color: var(--cream);
|
||||
border-radius: var(--radius);
|
||||
padding: 1.1rem 1.25rem;
|
||||
margin: 0 0 1.5rem;
|
||||
}
|
||||
.qcard-label {
|
||||
font-size: 0.78rem;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
color: var(--brass);
|
||||
margin: 0 0 0.4rem;
|
||||
}
|
||||
.qcard-text {
|
||||
margin: 0;
|
||||
font-size: 1.05rem;
|
||||
white-space: pre-wrap;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
.field label {
|
||||
display: block;
|
||||
font-weight: 600;
|
||||
font-size: 0.9rem;
|
||||
margin: 0 0 0.4rem;
|
||||
}
|
||||
textarea {
|
||||
width: 100%;
|
||||
font: inherit;
|
||||
padding: 0.7rem 0.8rem;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--radius);
|
||||
background: #fff;
|
||||
color: var(--ink);
|
||||
resize: vertical;
|
||||
}
|
||||
textarea:focus-visible {
|
||||
outline: 2px solid var(--brass);
|
||||
outline-offset: 1px;
|
||||
}
|
||||
.hint {
|
||||
font-size: 0.82rem;
|
||||
color: var(--ink-soft);
|
||||
margin: 0.4rem 0 0;
|
||||
}
|
||||
.btn {
|
||||
display: inline-block;
|
||||
margin-top: 1rem;
|
||||
font-family: Marcellus, Georgia, serif;
|
||||
font-size: 0.9rem;
|
||||
letter-spacing: 0.04em;
|
||||
border: 0;
|
||||
border-radius: var(--radius-pill);
|
||||
background: var(--brass);
|
||||
color: var(--midnight);
|
||||
padding: 0.7rem 1.5rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
.btn:hover {
|
||||
background: var(--brass-bright);
|
||||
}
|
||||
.btn:focus-visible {
|
||||
outline: 2px solid var(--midnight);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
.btn[disabled] {
|
||||
opacity: 0.6;
|
||||
cursor: default;
|
||||
}
|
||||
.status {
|
||||
margin: 0.9rem 0 0;
|
||||
font-weight: 500;
|
||||
min-height: 1.2em;
|
||||
}
|
||||
</style>
|
||||
|
||||
<!-- 1) Read the credential, then take it OUT of the address bar.
|
||||
The hash arrives as ?has=... or ?hash=... (both spellings accepted, 'has' first).
|
||||
The page keeps it in memory for the GET, the PATCH and a retry; it must never sit in
|
||||
the URL, because Plausible reports location.href and a shared/forwarded link would
|
||||
leak it. Stripping the query BEFORE the tracker is loaded is what keeps it out of
|
||||
analytics. All wire names and key lists live here, in one place. -->
|
||||
<script>
|
||||
window.O4P = {
|
||||
// The node webhook: holds the question (GET) and receives the answer (PATCH).
|
||||
webhook: "https://n8n.odoo4projects.com/webhook/ODOO4projects/odoo_experts_quote",
|
||||
// Query-string spellings to read, in order. The brief wrote "has" twice, so "has"
|
||||
// wins; "hash" is accepted as the identical fallback. Confirm with the owner which
|
||||
// spelling the emailed links will carry.
|
||||
hashKeys: ["has", "hash"],
|
||||
// Wire name used OUTWARD, both directions: GET query param and PATCH body key.
|
||||
hashWireKey: "has",
|
||||
// PATCH body key carrying the freelancer's answer.
|
||||
answerWireKey: "answer",
|
||||
// The ONLY response keys this page may render. Anything not listed is never read,
|
||||
// so a client's email/name/phone sitting in the same object cannot reach the page.
|
||||
questionKeys: ["question", "brief", "message", "text"]
|
||||
};
|
||||
|
||||
(function () {
|
||||
var params = new URLSearchParams(location.search);
|
||||
var hash = "";
|
||||
for (var i = 0; i < window.O4P.hashKeys.length; i++) {
|
||||
var v = params.get(window.O4P.hashKeys[i]);
|
||||
if (v) { hash = v; break; }
|
||||
}
|
||||
window.__ANSWER_HASH = hash;
|
||||
// Drop the query string from the address + history. No reload loses anything:
|
||||
// the hash now lives in memory above.
|
||||
if (location.search) history.replaceState(null, "", location.pathname);
|
||||
})();
|
||||
</script>
|
||||
|
||||
<!-- 2) Privacy-friendly analytics by Plausible (verbatim; the page now has no query). -->
|
||||
<script async src="https://plausible.office.odoo4projects.com/js/pa-NhA7Rf-79VopXFQXhSS4e.js"></script>
|
||||
<script>
|
||||
window.plausible=window.plausible||function(){(plausible.q=plausible.q||[]).push(arguments)},plausible.init=plausible.init||function(i){plausible.o=i||{}};
|
||||
plausible.init()
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<header class="topbar">
|
||||
<div class="wrap">
|
||||
<a class="brand" href="https://odoo4projects.com/">ODOO<span>4</span>projects</a>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<div class="wrap">
|
||||
<div class="card-wrap">
|
||||
<h1>Answer a question</h1>
|
||||
<p class="lede">
|
||||
A client asked you a question through ODOO4projects. Write your answer below and it
|
||||
goes back to us — we pass it on to them.
|
||||
</p>
|
||||
|
||||
<!-- Filled by the script at the foot of the page. Empty on purpose: with
|
||||
JavaScript off there is no way to read the question or send an answer, so an
|
||||
empty container + the noscript note below is the honest picture. -->
|
||||
<div id="app"></div>
|
||||
|
||||
<noscript>
|
||||
<div class="panel">
|
||||
<p class="state state-warn">This page needs JavaScript to load the question and to send your answer.</p>
|
||||
<p class="detail">
|
||||
Turn JavaScript on in your browser and open the link again — or reply to the email
|
||||
that brought you here.
|
||||
</p>
|
||||
</div>
|
||||
</noscript>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
var C = window.O4P;
|
||||
var app = document.getElementById("app");
|
||||
var hash = window.__ANSWER_HASH || "";
|
||||
|
||||
function el(tag, cls, text) {
|
||||
var n = document.createElement(tag);
|
||||
if (cls) n.className = cls;
|
||||
if (text != null) n.textContent = text; // textContent only: a question is never markup
|
||||
return n;
|
||||
}
|
||||
function show(nodes) {
|
||||
while (app.firstChild) app.removeChild(app.firstChild);
|
||||
var box = el("div", "panel");
|
||||
for (var i = 0; i < nodes.length; i++) box.appendChild(nodes[i]);
|
||||
app.appendChild(box);
|
||||
}
|
||||
function status(text, kind) {
|
||||
var p = el("p", "state" + (kind ? " state-" + kind : ""), text);
|
||||
p.setAttribute("role", "status");
|
||||
p.setAttribute("aria-live", "polite");
|
||||
return p;
|
||||
}
|
||||
function retryButton() {
|
||||
var b = el("button", "btn", "Try again");
|
||||
b.type = "button";
|
||||
b.addEventListener("click", load);
|
||||
return b;
|
||||
}
|
||||
|
||||
// The GET reply may be a bare object, an array, or wrapped in .data. Unwrap the
|
||||
// envelope only — the field names inside are decided by questionKeys.
|
||||
function unwrap(payload) {
|
||||
if (Array.isArray(payload)) return payload[0] || null;
|
||||
if (payload && typeof payload === "object") {
|
||||
if (Array.isArray(payload.data)) return payload.data[0] || null;
|
||||
if (Array.isArray(payload.items)) return payload.items[0] || null;
|
||||
return payload;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
function pickQuestion(obj) {
|
||||
if (!obj) return "";
|
||||
for (var i = 0; i < C.questionKeys.length; i++) {
|
||||
var v = obj[C.questionKeys[i]];
|
||||
if (typeof v === "string" && v.trim()) return v.trim();
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
function load() {
|
||||
if (!hash) {
|
||||
show([
|
||||
status("This link is incomplete — it arrived without its code.", "warn"),
|
||||
el("p", "detail", "Open it again from the email that brought you here.")
|
||||
]);
|
||||
return;
|
||||
}
|
||||
show([status("Loading your question…")]);
|
||||
|
||||
fetch(C.webhook + "?" + encodeURIComponent(C.hashWireKey) + "=" + encodeURIComponent(hash), {
|
||||
headers: { Accept: "application/json" }
|
||||
})
|
||||
.then(function (res) {
|
||||
if (!res.ok) {
|
||||
// Live behaviour for an unknown hash: 500 {"code":0,"message":"No item to return was found"}.
|
||||
return res.text().then(function (body) {
|
||||
var msg = "";
|
||||
try { msg = (JSON.parse(body) || {}).message || ""; } catch (e) { /* not json */ }
|
||||
show([
|
||||
status("This link is not valid, or it has expired.", "warn"),
|
||||
el("p", "detail", "The service answered " + res.status + (msg ? " — " + msg : "") + "."),
|
||||
el("p", "detail", "If you think that is wrong, reply to the email that brought you here.")
|
||||
]);
|
||||
});
|
||||
}
|
||||
return res.json().then(renderQuestion).catch(function () {
|
||||
show([
|
||||
status("We could not read the reply from the service.", "warn"),
|
||||
el("p", "detail", "The reply was not in a form we understand. Please try again."),
|
||||
retryButton()
|
||||
]);
|
||||
});
|
||||
})
|
||||
.catch(function () {
|
||||
// Network failure: the hash is still in memory, so a retry needs no new link.
|
||||
show([
|
||||
status("We could not load your question just now.", "warn"),
|
||||
el("p", "detail", "Your link has been kept, so you can try again."),
|
||||
retryButton()
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
function renderQuestion(payload) {
|
||||
var obj = unwrap(payload);
|
||||
var question = pickQuestion(obj);
|
||||
if (!question) {
|
||||
// Honest about a shape we did not expect: name the fields, never a value. A
|
||||
// field name cannot identify the client; a value might.
|
||||
var names = obj && typeof obj === "object" ? Object.keys(obj) : [];
|
||||
show([
|
||||
status("We could not read the question from the reply from the service.", "warn"),
|
||||
el("p", "detail",
|
||||
names.length
|
||||
? "The reply carried these fields: " + names.join(", ") + "."
|
||||
: "The reply carried no readable data.")
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
var card = el("div", "qcard");
|
||||
card.appendChild(el("p", "qcard-label", "The question"));
|
||||
card.appendChild(el("blockquote", "qcard-text", question));
|
||||
|
||||
var form = el("form", "answer-form");
|
||||
form.setAttribute("novalidate", "novalidate"); // JS does the check, announced below
|
||||
|
||||
var field = el("div", "field");
|
||||
var label = el("label", "", "Your answer");
|
||||
label.setAttribute("for", "answerText");
|
||||
var ta = el("textarea");
|
||||
ta.id = "answerText";
|
||||
ta.name = C.answerWireKey;
|
||||
ta.rows = 8;
|
||||
ta.required = true;
|
||||
ta.setAttribute("aria-describedby", "answerHint");
|
||||
var hint = el("p", "hint", "Your answer goes back to ODOO4projects, who pass it on. Write it as you would to the client.");
|
||||
hint.id = "answerHint";
|
||||
field.appendChild(label);
|
||||
field.appendChild(ta);
|
||||
field.appendChild(hint);
|
||||
|
||||
var btn = el("button", "btn", "Send answer");
|
||||
btn.type = "submit";
|
||||
var line = el("p", "status");
|
||||
line.id = "answerStatus";
|
||||
line.setAttribute("role", "status");
|
||||
line.setAttribute("aria-live", "polite");
|
||||
|
||||
function setLine(text, kind) {
|
||||
line.className = "status" + (kind ? " state-" + kind : "");
|
||||
line.textContent = text;
|
||||
}
|
||||
|
||||
form.addEventListener("submit", function (event) {
|
||||
event.preventDefault();
|
||||
var answer = ta.value.trim();
|
||||
if (!answer) {
|
||||
ta.setAttribute("aria-invalid", "true");
|
||||
setLine("Please write your answer first.", "warn");
|
||||
ta.focus();
|
||||
return;
|
||||
}
|
||||
ta.removeAttribute("aria-invalid");
|
||||
|
||||
// The body shape, in one constant (C.hashWireKey + C.answerWireKey):
|
||||
// {"has":"<hash>","answer":"<text>"}
|
||||
// PATCH, not POST: the question record already exists (the client's submission
|
||||
// created it), so this answer is a PARTIAL UPDATE of it — POST risks creating a
|
||||
// second record. Match this method and these two keys in the n8n workflow.
|
||||
var body = {};
|
||||
body[C.hashWireKey] = hash;
|
||||
body[C.answerWireKey] = answer;
|
||||
|
||||
btn.disabled = true;
|
||||
setLine("Sending…", null);
|
||||
fetch(C.webhook, {
|
||||
method: "PATCH",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(body)
|
||||
})
|
||||
.then(function (res) {
|
||||
if (res.ok) {
|
||||
// A RECEIPT, not a delivery claim: n8n answers 200 before the workflow
|
||||
// runs, so this page cannot know the answer reached anyone. No time is
|
||||
// promised. The receipt shows ONLY on a 2xx.
|
||||
setLine("Thank you — we have your answer.", "ok");
|
||||
form.reset();
|
||||
return;
|
||||
}
|
||||
// Non-2xx: never show the receipt. A method the route refuses (404/405/501)
|
||||
// is a clear failure, not a transient one — say so plainly and KEEP the
|
||||
// answer in the box so it can be retried.
|
||||
var refused = res.status === 404 || res.status === 405 || res.status === 501;
|
||||
setLine(
|
||||
refused
|
||||
? "We could not save your answer. Please reply to the email that brought you here."
|
||||
: "That did not send. Please try again.",
|
||||
"err"
|
||||
);
|
||||
})
|
||||
.catch(function () {
|
||||
// Network failure: the hash stays in memory, the answer stays in the box.
|
||||
setLine("That did not send. Please try again.", "err");
|
||||
})
|
||||
.then(function () { btn.disabled = false; });
|
||||
});
|
||||
|
||||
form.appendChild(field);
|
||||
form.appendChild(btn);
|
||||
form.appendChild(line);
|
||||
|
||||
show([card, form]);
|
||||
}
|
||||
|
||||
load();
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,179 @@
|
||||
<!doctype html>
|
||||
<html lang="en-US">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Your email is confirmed — ODOO4projects</title>
|
||||
<meta name="description" content="Private page: your email is confirmed and your card is in the Odoo Freelancer Directory." />
|
||||
|
||||
<!-- Private utility page. It is reached ONLY by the confirmation link we email, never by
|
||||
search. noindex+nofollow here, absent from sitemap.xml and llms.txt, and linked from
|
||||
nowhere on the site. See [[draft-page-convention]] for the same five rule. -->
|
||||
<meta name="robots" content="noindex, nofollow" />
|
||||
<!-- The confirmation is completed by the workflow, so this address may carry anything the
|
||||
redirect appends. Nothing from the query string is ever rendered or read: the page is
|
||||
identical with a bare URL and with something appended, and the query is dropped from the
|
||||
address (below) before the tracker loads. -->
|
||||
<meta name="referrer" content="no-referrer" />
|
||||
<link rel="canonical" href="https://odoo4projects.com/freelancer-confirmed/" />
|
||||
<meta property="og:url" content="https://odoo4projects.com/freelancer-confirmed/" />
|
||||
<meta property="og:title" content="Your email is confirmed — ODOO4projects" />
|
||||
<meta property="og:description" content="Your card is in the Odoo Freelancer Directory." />
|
||||
<meta property="og:type" content="website" />
|
||||
<link rel="icon" href="/images/favicon.ico" />
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Marcellus&display=swap"
|
||||
media="print"
|
||||
onload="this.media='all'"
|
||||
/>
|
||||
<noscript><link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Marcellus&display=swap" /></noscript>
|
||||
<link rel="stylesheet" href="/style.css?v=061517baef" />
|
||||
|
||||
<style>
|
||||
/* Utility page, same shell as /freelancer-edit/ and /feedback/: brand line, one card, the
|
||||
site tokens (:root in style.css) and the CI palette only. No hero, no footer, no site
|
||||
chrome beyond the brand line. */
|
||||
* { box-sizing: border-box; }
|
||||
body {
|
||||
margin: 0;
|
||||
background: var(--cream);
|
||||
color: var(--ink);
|
||||
font-family: Inter, "Segoe UI", sans-serif;
|
||||
line-height: 1.6;
|
||||
}
|
||||
.wrap { width: 86%; max-width: 1200px; margin: 0 auto; }
|
||||
.topbar { background: var(--midnight); border-bottom: 1px solid var(--line-brass); padding: 0.85rem 0; }
|
||||
.brand {
|
||||
font-family: Marcellus, Georgia, serif;
|
||||
font-size: 1.15rem;
|
||||
letter-spacing: 0.04em;
|
||||
color: #fff;
|
||||
text-decoration: none;
|
||||
}
|
||||
.brand span { color: var(--brass); }
|
||||
main { padding: 2.5rem 0 4rem; }
|
||||
.card-wrap { max-width: 760px; margin: 0 auto; }
|
||||
h1 {
|
||||
font-family: Marcellus, Georgia, serif;
|
||||
font-weight: 400;
|
||||
font-size: clamp(1.5rem, 4vw, 2.2rem);
|
||||
letter-spacing: 0.02em;
|
||||
margin: 0 0 0.4rem;
|
||||
}
|
||||
.lede { color: var(--ink-soft); margin: 0 0 1rem; }
|
||||
.panel {
|
||||
background: var(--ivory);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: 1.5rem;
|
||||
box-shadow: var(--shadow-rest);
|
||||
}
|
||||
.panel + .panel { margin-top: 1.25rem; }
|
||||
h2 {
|
||||
font-family: Marcellus, Georgia, serif;
|
||||
font-weight: 400;
|
||||
font-size: 1.15rem;
|
||||
letter-spacing: 0.02em;
|
||||
margin: 0 0 0.6rem;
|
||||
}
|
||||
.detail { margin: 0.5rem 0 0; font-size: 0.9rem; color: var(--ink-soft); }
|
||||
.detail:first-of-type { margin-top: 0; }
|
||||
|
||||
/* The page's ONE primary action is the feedback link; the directory link is a plain link. */
|
||||
.btn {
|
||||
display: inline-block;
|
||||
font-family: Marcellus, Georgia, serif;
|
||||
font-size: 1rem;
|
||||
letter-spacing: 0.04em;
|
||||
border: 0;
|
||||
border-radius: var(--radius-pill);
|
||||
background: var(--brass);
|
||||
color: var(--midnight);
|
||||
padding: 0.85rem 2rem;
|
||||
margin-top: 1rem;
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
}
|
||||
.btn:hover { background: var(--brass-bright); }
|
||||
/* The anchor needs the text colour kept on hover: the site's global a:hover ({color:
|
||||
var(--brass)}) outranks a lone .btn, and brass on brass-bright is unreadable. */
|
||||
a.btn, a.btn:hover { color: var(--midnight); }
|
||||
.btn:focus-visible { outline: 2px solid var(--midnight); outline-offset: 2px; }
|
||||
.link-plain { color: var(--brass-deep); font-weight: 600; }
|
||||
</style>
|
||||
|
||||
<!-- The confirmation redirect may append anything (a name, an id, a tracking token). NOTHING
|
||||
from the query string is read or rendered: this page has one state, whatever it is handed.
|
||||
The query is dropped from the address + the history entry BEFORE the tracker is loaded —
|
||||
Plausible reports location.href, so stripping it first is what keeps anything appended out
|
||||
of the analytics payload (the same care as /freelancer-edit/). With JavaScript off the
|
||||
query stays in the address bar and still nothing renders it. -->
|
||||
<script>
|
||||
(function () {
|
||||
if (location.search) history.replaceState(null, "", location.pathname);
|
||||
})();
|
||||
</script>
|
||||
|
||||
<!-- Privacy-friendly analytics by Plausible (verbatim; the page now carries no query). -->
|
||||
<script async src="https://plausible.office.odoo4projects.com/js/pa-NhA7Rf-79VopXFQXhSS4e.js"></script>
|
||||
<script>
|
||||
window.plausible=window.plausible||function(){(plausible.q=plausible.q||[]).push(arguments)},plausible.init=plausible.init||function(i){plausible.o=i||{}};
|
||||
plausible.init()
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<header class="topbar">
|
||||
<div class="wrap">
|
||||
<a class="brand" href="https://odoo4projects.com/">ODOO<span>4</span>projects</a>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<div class="wrap">
|
||||
<div class="card-wrap">
|
||||
<h1>Your email is confirmed</h1>
|
||||
<p class="lede">
|
||||
Thank you — your email address is confirmed, and your card is now in the Odoo Freelancer
|
||||
Directory.
|
||||
</p>
|
||||
|
||||
<!-- 1. The page's primary action: the feedback. On its own panel, first, brass and larger. -->
|
||||
<div class="panel">
|
||||
<h2>How did the signup go?</h2>
|
||||
<p class="detail">
|
||||
The signup is the part we most want to get right, and you have just been through it.
|
||||
Tell us how it went — every answer helps us improve it.
|
||||
</p>
|
||||
<a class="btn" href="https://odoo4projects.com/feedback/">Give feedback on the signup</a>
|
||||
</div>
|
||||
|
||||
<!-- 2. See yourself in the directory. -->
|
||||
<div class="panel">
|
||||
<h2>See your card in the directory</h2>
|
||||
<p class="detail">
|
||||
Everything on a card is the freelancer's own words.
|
||||
</p>
|
||||
<p class="detail">
|
||||
<a
|
||||
class="link-plain"
|
||||
href="https://odoo4projects.com/odoo-community-developers/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>Open the Odoo Freelancer Directory (opens in a new window)</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- 3. Nobody is stuck with what they typed. One line, and the credential stays where it
|
||||
belongs: the edit link is the one we email, so it is not linked from here. -->
|
||||
<p class="detail">
|
||||
You can change or remove your profile at any time — the link for that is in the email we
|
||||
send you.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,813 @@
|
||||
<!doctype html>
|
||||
<html lang="en-US">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Edit your listing — ODOO4projects</title>
|
||||
<meta name="description" content="Private page: a listed Odoo freelancer edits or deletes their own listing." />
|
||||
|
||||
<!-- Private utility page. It is reached ONLY by a link we email, never by search.
|
||||
noindex+nofollow here, absent from sitemap.xml and llms.txt, and linked from nowhere
|
||||
on the site. See [[draft-page-convention]] for the same five rule. -->
|
||||
<meta name="robots" content="noindex, nofollow" />
|
||||
<!-- While the link is open the address carries a credential. No claim-by-subdomain of the
|
||||
hash travels to a third party (Google Fonts, and every other cross-origin request). -->
|
||||
<meta name="referrer" content="no-referrer" />
|
||||
<link rel="canonical" href="https://odoo4projects.com/freelancer-edit/" />
|
||||
<!-- The ONE path the emailed link uses; mirrors /freelancer-answer/ deliberately. -->
|
||||
<meta property="og:url" content="https://odoo4projects.com/freelancer-edit/" />
|
||||
<meta property="og:title" content="Edit your listing — ODOO4projects" />
|
||||
<meta property="og:type" content="website" />
|
||||
<link rel="icon" href="/images/favicon.ico" />
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Marcellus&display=swap"
|
||||
media="print"
|
||||
onload="this.media='all'"
|
||||
/>
|
||||
<noscript><link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Marcellus&display=swap" /></noscript>
|
||||
<link rel="stylesheet" href="/style.css?v=061517baef" />
|
||||
|
||||
<style>
|
||||
/* Utility page. Deliberately small: no hero, no footer, no site chrome beyond a brand
|
||||
line. Reuses the site tokens (:root in style.css) and the CI palette only. */
|
||||
* { box-sizing: border-box; }
|
||||
body {
|
||||
margin: 0;
|
||||
background: var(--cream);
|
||||
color: var(--ink);
|
||||
font-family: Inter, "Segoe UI", sans-serif;
|
||||
line-height: 1.6;
|
||||
}
|
||||
.wrap { width: 86%; max-width: 1200px; margin: 0 auto; }
|
||||
.topbar { background: var(--midnight); border-bottom: 1px solid var(--line-brass); padding: 0.85rem 0; }
|
||||
.brand {
|
||||
font-family: Marcellus, Georgia, serif;
|
||||
font-size: 1.15rem;
|
||||
letter-spacing: 0.04em;
|
||||
color: #fff;
|
||||
text-decoration: none;
|
||||
}
|
||||
.brand span { color: var(--brass); }
|
||||
main { padding: 2.5rem 0 4rem; }
|
||||
.card-wrap { max-width: 720px; margin: 0 auto; }
|
||||
h1 {
|
||||
font-family: Marcellus, Georgia, serif;
|
||||
font-weight: 400;
|
||||
font-size: clamp(1.5rem, 4vw, 2.2rem);
|
||||
letter-spacing: 0.02em;
|
||||
margin: 0 0 0.4rem;
|
||||
}
|
||||
.lede { color: var(--ink-soft); margin: 0 0 1.75rem; }
|
||||
.panel {
|
||||
background: var(--ivory);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: 1.5rem;
|
||||
box-shadow: var(--shadow-rest);
|
||||
}
|
||||
.state { margin: 0; font-weight: 500; }
|
||||
.state-warn, .state-err { color: var(--brass-deep); }
|
||||
.state-ok { color: var(--success); }
|
||||
.detail { margin: 0.5rem 0 0; font-size: 0.9rem; color: var(--ink-soft); }
|
||||
.who {
|
||||
font-size: 0.8rem;
|
||||
letter-spacing: 0.06em;
|
||||
text-transform: uppercase;
|
||||
color: var(--brass-deep);
|
||||
margin: 0 0 1rem;
|
||||
}
|
||||
|
||||
/* ---- The form ---- */
|
||||
.field { margin: 0 0 1.1rem; }
|
||||
.field > label { display: block; font-weight: 600; font-size: 0.9rem; margin: 0 0 0.35rem; }
|
||||
input[type="text"], input[type="url"], input[type="email"], textarea, select {
|
||||
width: 100%;
|
||||
font: inherit;
|
||||
padding: 0.6rem 0.7rem;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--radius);
|
||||
background: #fff;
|
||||
color: var(--ink);
|
||||
}
|
||||
textarea { resize: vertical; }
|
||||
input:focus-visible, textarea:focus-visible, select:focus-visible, button:focus-visible {
|
||||
outline: 2px solid var(--brass);
|
||||
outline-offset: 1px;
|
||||
}
|
||||
.hint { font-size: 0.82rem; color: var(--ink-soft); margin: 0.35rem 0 0; }
|
||||
.counter { font-size: 0.78rem; color: var(--ink-soft); margin: 0.3rem 0 0; }
|
||||
.counter[data-over="1"] { color: var(--brass-deep); font-weight: 600; }
|
||||
|
||||
fieldset { border: 1px solid var(--line); border-radius: var(--radius); padding: 0.9rem 1rem 0.4rem; margin: 0 0 1.1rem; }
|
||||
legend { font-weight: 600; font-size: 0.9rem; padding: 0 0.35rem; }
|
||||
.row { display: flex; align-items: center; gap: 0.7rem; margin: 0 0 0.7rem; }
|
||||
.row label { flex: 0 0 6.5rem; font-size: 0.85rem; }
|
||||
.row input[type="range"] { flex: 1 1 auto; min-width: 0; }
|
||||
.row output { flex: 0 0 1.6rem; text-align: center; font-variant-numeric: tabular-nums; font-weight: 600; }
|
||||
.total { font-size: 0.85rem; color: var(--ink-soft); margin: 0 0 0.4rem; }
|
||||
.total[data-bad="1"] { color: var(--brass-deep); font-weight: 600; }
|
||||
|
||||
.btn {
|
||||
display: inline-block;
|
||||
font-family: Marcellus, Georgia, serif;
|
||||
font-size: 0.9rem;
|
||||
letter-spacing: 0.04em;
|
||||
border: 0;
|
||||
border-radius: var(--radius-pill);
|
||||
background: var(--brass);
|
||||
color: var(--midnight);
|
||||
padding: 0.7rem 1.5rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
.btn:hover { background: var(--brass-bright); }
|
||||
.btn:focus-visible { outline: 2px solid var(--midnight); outline-offset: 2px; }
|
||||
.btn[disabled] { opacity: 0.6; cursor: default; }
|
||||
.status { margin: 0.9rem 0 0; font-weight: 500; min-height: 1.2em; }
|
||||
|
||||
/* ---- Delete ---- */
|
||||
.danger { margin-top: 1.75rem; border-top: 1px solid var(--line); padding-top: 1.25rem; }
|
||||
.danger h2 { font-family: Marcellus, Georgia, serif; font-weight: 400; font-size: 1.1rem; margin: 0 0 0.4rem; }
|
||||
.danger p { font-size: 0.88rem; color: var(--ink-soft); margin: 0 0 0.8rem; }
|
||||
.confirm { display: flex; align-items: flex-start; gap: 0.55rem; margin: 0 0 0.9rem; font-size: 0.88rem; }
|
||||
.confirm input { margin-top: 0.25rem; }
|
||||
.btn-danger { background: transparent; border: 1px solid var(--brass-deep); color: var(--brass-deep); }
|
||||
.btn-danger:hover { background: var(--brass-deep); color: #fff; }
|
||||
.btn-danger:focus-visible { outline: 2px solid var(--midnight); outline-offset: 2px; }
|
||||
.photo-readonly { display: flex; align-items: center; gap: 0.8rem; }
|
||||
.photo-readonly img { width: 56px; height: 56px; border-radius: 50%; object-fit: cover; }
|
||||
.photo-readonly .mono {
|
||||
width: 56px; height: 56px; border-radius: 50%;
|
||||
display: grid; place-items: center;
|
||||
background: var(--midnight); color: var(--brass);
|
||||
font-family: Marcellus, Georgia, serif;
|
||||
}
|
||||
</style>
|
||||
|
||||
<!-- The 15-point rule, in ONE copy for both forms (this one and the application form). -->
|
||||
<script src="/static/focus-rule.js?v=22ac72a8f0"></script>
|
||||
<script src="/static/photo-upload.js?v=c17638457b"></script>
|
||||
<script src="/static/expert-image.js?v=bad5d8c733"></script>
|
||||
|
||||
<!-- 1) Read the credential, then take it OUT of the address bar.
|
||||
The hash arrives as ?has=... or ?hash=... (both spellings accepted, 'has' first — the
|
||||
same list the answer page reads, so the two emailed links behave identically). The page
|
||||
keeps it in memory for the GET, the save, the delete and a retry; it must never sit in
|
||||
the URL, because Plausible reports location.href. Stripping the query BEFORE the tracker
|
||||
is loaded is what keeps it out of analytics. All wire names and key lists live here. -->
|
||||
<script>
|
||||
window.O4E = {
|
||||
// The node webhook: holds this person's listing (GET) and receives the save + delete.
|
||||
webhook: "https://n8n.odoo4projects.com/webhook/ODOO4projects/odoo_experts_edit",
|
||||
// Query-string spellings to read, in order. Same list as the answer page: the two
|
||||
// emailed links are one shape. Confirm with the owner which spelling the mails carry.
|
||||
hashKeys: ["has", "hash"],
|
||||
// Wire name used OUTWARD on the GET query string.
|
||||
hashWireKey: "has",
|
||||
// SAVE: PATCH the existing record, carrying BOTH the hash (the credential, same key
|
||||
// the delete already sends) and the record id. An id alone authorises nothing: record
|
||||
// ids are small sequential integers, publicly visible on every directory card, so a
|
||||
// request without the hash is refused before it is sent. Match this method in n8n.
|
||||
saveMethod: "PATCH",
|
||||
// DELETE: unverified. A route that refuses it (404/405/501) is a clear failure and
|
||||
// this page never reports success on a non-2xx. Match this method in the workflow.
|
||||
deleteMethod: "DELETE",
|
||||
// How the GET reply is read, in ONE place. The primary name is what the directory
|
||||
// webhook already returns, so the two endpoints agree; a lower-case variant is taken
|
||||
// as the identical fallback. A key NOT listed here is never read.
|
||||
aliases: {
|
||||
id: ["id"],
|
||||
Name: ["Name", "name"],
|
||||
Focus: ["Focus", "focus"],
|
||||
description: ["description", "Description"],
|
||||
Country: ["Country", "country"],
|
||||
OCA_link: ["OCA_link", "oca_link"],
|
||||
// The four profile links. The primary name is the LIVE wire key, casing and all —
|
||||
// "Linkedin", "github", "upwork", "fiverr" are deliberately mixed and must never be
|
||||
// normalised (a normalised key is the silent-drop class that ate odoo_versions). A
|
||||
// same-word casing variant is the identical fallback, the way Name/name already works;
|
||||
// "github" is lower-case on the wire, so it is read exactly as it is, with no variant.
|
||||
Linkedin: ["Linkedin", "linkedin"], github: ["github"], upwork: ["upwork", "Upwork"], fiverr: ["fiverr", "Fiverr"],
|
||||
functional: ["functional"], system: ["system"], modules: ["modules"],
|
||||
integration: ["integration"], ai: ["ai"],
|
||||
english: ["english"], german: ["german"], spanish: ["spanish"]
|
||||
},
|
||||
focusAxes: ["functional", "system", "modules", "integration", "ai"],
|
||||
focusAxisLabels: {
|
||||
functional: "Functional", system: "System", modules: "Modules",
|
||||
integration: "Integration", ai: "AI"
|
||||
},
|
||||
focusTotal: window.O4PFocus.TOTAL,
|
||||
focusMax: 5,
|
||||
focusMaxChars: 70,
|
||||
descMaxChars: 230,
|
||||
langs: [["english", "English"], ["german", "German"], ["spanish", "Spanish"]],
|
||||
// Country VALUE FORMAT the select sends, same as the other forms: the ISO alpha-2 code.
|
||||
countryValueFormat: "code"
|
||||
};
|
||||
|
||||
(function () {
|
||||
var params = new URLSearchParams(location.search);
|
||||
var hash = "";
|
||||
for (var i = 0; i < window.O4E.hashKeys.length; i++) {
|
||||
var v = params.get(window.O4E.hashKeys[i]);
|
||||
if (v) { hash = v; break; }
|
||||
}
|
||||
window.__EDIT_HASH = hash;
|
||||
// Drop the query from the address + history. The hash lives in memory above.
|
||||
if (location.search) history.replaceState(null, "", location.pathname);
|
||||
})();
|
||||
</script>
|
||||
|
||||
<!-- 2) Privacy-friendly analytics by Plausible (verbatim; the page now has no query). -->
|
||||
<script async src="https://plausible.office.odoo4projects.com/js/pa-NhA7Rf-79VopXFQXhSS4e.js"></script>
|
||||
<script>
|
||||
window.plausible=window.plausible||function(){(plausible.q=plausible.q||[]).push(arguments)},plausible.init=plausible.init||function(i){plausible.o=i||{}};
|
||||
plausible.init()
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<header class="topbar">
|
||||
<div class="wrap">
|
||||
<a class="brand" href="https://odoo4projects.com/">ODOO<span>4</span>projects</a>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<div class="wrap">
|
||||
<div class="card-wrap">
|
||||
<h1>Edit your listing</h1>
|
||||
<p class="lede">
|
||||
This is your own listing in the Odoo Freelancer Directory. Change what you like here
|
||||
and save it — or delete the listing for good.
|
||||
</p>
|
||||
|
||||
<!-- Filled by the script at the foot of the page. Empty on purpose: with JavaScript
|
||||
off there is no way to load the listing or save a change, so an empty container +
|
||||
the noscript note below is the honest picture. -->
|
||||
<div id="app"></div>
|
||||
|
||||
<noscript>
|
||||
<div class="panel">
|
||||
<p class="state state-warn">This page needs JavaScript to load your listing and to save your changes.</p>
|
||||
<p class="detail">
|
||||
Turn JavaScript on in your browser and open the link again — or reply to the email
|
||||
that brought you here.
|
||||
</p>
|
||||
</div>
|
||||
</noscript>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<script>
|
||||
var COUNTRIES = [["AD", "Andorra"], ["AE", "United Arab Emirates"], ["AF", "Afghanistan"], ["AG", "Antigua and Barbuda"], ["AI", "Anguilla"], ["AL", "Albania"], ["AM", "Armenia"], ["AO", "Angola"], ["AQ", "Antarctica"], ["AR", "Argentina"], ["AS", "American Samoa"], ["AT", "Austria"], ["AU", "Australia"], ["AW", "Aruba"], ["AX", "Aland"], ["AZ", "Azerbaijan"], ["BA", "Bosnia and Herzegovina"], ["BB", "Barbados"], ["BD", "Bangladesh"], ["BE", "Belgium"], ["BF", "Burkina Faso"], ["BG", "Bulgaria"], ["BH", "Bahrain"], ["BI", "Burundi"], ["BJ", "Benin"], ["BL", "Saint Barthelemy"], ["BM", "Bermuda"], ["BN", "Brunei"], ["BO", "Bolivia"], ["BQ", "Bonaire"], ["BR", "Brazil"], ["BS", "Bahamas"], ["BT", "Bhutan"], ["BV", "Bouvet Island"], ["BW", "Botswana"], ["BY", "Belarus"], ["BZ", "Belize"], ["CA", "Canada"], ["CC", "Cocos (Keeling) Islands"], ["CD", "Democratic Republic of the Congo"], ["CF", "Central African Republic"], ["CG", "Republic of the Congo"], ["CH", "Switzerland"], ["CI", "Ivory Coast"], ["CK", "Cook Islands"], ["CL", "Chile"], ["CM", "Cameroon"], ["CN", "China"], ["CO", "Colombia"], ["CR", "Costa Rica"], ["CU", "Cuba"], ["CV", "Cape Verde"], ["CW", "Curacao"], ["CX", "Christmas Island"], ["CY", "Cyprus"], ["CZ", "Czech Republic"], ["DE", "Germany"], ["DJ", "Djibouti"], ["DK", "Denmark"], ["DM", "Dominica"], ["DO", "Dominican Republic"], ["DZ", "Algeria"], ["EC", "Ecuador"], ["EE", "Estonia"], ["EG", "Egypt"], ["EH", "Western Sahara"], ["ER", "Eritrea"], ["ES", "Spain"], ["ET", "Ethiopia"], ["FI", "Finland"], ["FJ", "Fiji"], ["FK", "Falkland Islands"], ["FM", "Micronesia"], ["FO", "Faroe Islands"], ["FR", "France"], ["GA", "Gabon"], ["GB", "United Kingdom"], ["GD", "Grenada"], ["GE", "Georgia"], ["GF", "French Guiana"], ["GG", "Guernsey"], ["GH", "Ghana"], ["GI", "Gibraltar"], ["GL", "Greenland"], ["GM", "Gambia"], ["GN", "Guinea"], ["GP", "Guadeloupe"], ["GQ", "Equatorial Guinea"], ["GR", "Greece"], ["GS", "South Georgia and the South Sandwich Islands"], ["GT", "Guatemala"], ["GU", "Guam"], ["GW", "Guinea-Bissau"], ["GY", "Guyana"], ["HK", "Hong Kong"], ["HM", "Heard Island and McDonald Islands"], ["HN", "Honduras"], ["HR", "Croatia"], ["HT", "Haiti"], ["HU", "Hungary"], ["ID", "Indonesia"], ["IE", "Ireland"], ["IL", "Israel"], ["IM", "Isle of Man"], ["IN", "India"], ["IO", "British Indian Ocean Territory"], ["IQ", "Iraq"], ["IR", "Iran"], ["IS", "Iceland"], ["IT", "Italy"], ["JE", "Jersey"], ["JM", "Jamaica"], ["JO", "Jordan"], ["JP", "Japan"], ["KE", "Kenya"], ["KG", "Kyrgyzstan"], ["KH", "Cambodia"], ["KI", "Kiribati"], ["KM", "Comoros"], ["KN", "Saint Kitts and Nevis"], ["KP", "North Korea"], ["KR", "South Korea"], ["KW", "Kuwait"], ["KY", "Cayman Islands"], ["KZ", "Kazakhstan"], ["LA", "Laos"], ["LB", "Lebanon"], ["LC", "Saint Lucia"], ["LI", "Liechtenstein"], ["LK", "Sri Lanka"], ["LR", "Liberia"], ["LS", "Lesotho"], ["LT", "Lithuania"], ["LU", "Luxembourg"], ["LV", "Latvia"], ["LY", "Libya"], ["MA", "Morocco"], ["MC", "Monaco"], ["MD", "Moldova"], ["ME", "Montenegro"], ["MF", "Saint Martin"], ["MG", "Madagascar"], ["MH", "Marshall Islands"], ["MK", "North Macedonia"], ["ML", "Mali"], ["MM", "Myanmar (Burma)"], ["MN", "Mongolia"], ["MO", "Macao"], ["MP", "Northern Mariana Islands"], ["MQ", "Martinique"], ["MR", "Mauritania"], ["MS", "Montserrat"], ["MT", "Malta"], ["MU", "Mauritius"], ["MV", "Maldives"], ["MW", "Malawi"], ["MX", "Mexico"], ["MY", "Malaysia"], ["MZ", "Mozambique"], ["NA", "Namibia"], ["NC", "New Caledonia"], ["NE", "Niger"], ["NF", "Norfolk Island"], ["NG", "Nigeria"], ["NI", "Nicaragua"], ["NL", "Netherlands"], ["NO", "Norway"], ["NP", "Nepal"], ["NR", "Nauru"], ["NU", "Niue"], ["NZ", "New Zealand"], ["OM", "Oman"], ["PA", "Panama"], ["PE", "Peru"], ["PF", "French Polynesia"], ["PG", "Papua New Guinea"], ["PH", "Philippines"], ["PK", "Pakistan"], ["PL", "Poland"], ["PM", "Saint Pierre and Miquelon"], ["PN", "Pitcairn Islands"], ["PR", "Puerto Rico"], ["PS", "Palestine"], ["PT", "Portugal"], ["PW", "Palau"], ["PY", "Paraguay"], ["QA", "Qatar"], ["RE", "Reunion"], ["RO", "Romania"], ["RS", "Serbia"], ["RU", "Russia"], ["RW", "Rwanda"], ["SA", "Saudi Arabia"], ["SB", "Solomon Islands"], ["SC", "Seychelles"], ["SD", "Sudan"], ["SE", "Sweden"], ["SG", "Singapore"], ["SH", "Saint Helena"], ["SI", "Slovenia"], ["SJ", "Svalbard and Jan Mayen"], ["SK", "Slovakia"], ["SL", "Sierra Leone"], ["SM", "San Marino"], ["SN", "Senegal"], ["SO", "Somalia"], ["SR", "Suriname"], ["SS", "South Sudan"], ["ST", "Sao Tome and Principe"], ["SV", "El Salvador"], ["SX", "Sint Maarten"], ["SY", "Syria"], ["SZ", "Eswatini"], ["TC", "Turks and Caicos Islands"], ["TD", "Chad"], ["TF", "French Southern Territories"], ["TG", "Togo"], ["TH", "Thailand"], ["TJ", "Tajikistan"], ["TK", "Tokelau"], ["TL", "East Timor"], ["TM", "Turkmenistan"], ["TN", "Tunisia"], ["TO", "Tonga"], ["TR", "Turkey"], ["TT", "Trinidad and Tobago"], ["TV", "Tuvalu"], ["TW", "Taiwan"], ["TZ", "Tanzania"], ["UA", "Ukraine"], ["UG", "Line truncated
|
||||
</script>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
var C = window.O4E;
|
||||
var CG = window.O4PFocus; // the shared 15-point rule (static/focus-rule.js)
|
||||
var app = document.getElementById("app");
|
||||
var hash = window.__EDIT_HASH || "";
|
||||
var rec = null; // this person's record, once loaded
|
||||
|
||||
function el(tag, cls, text) {
|
||||
var n = document.createElement(tag);
|
||||
if (cls) n.className = cls;
|
||||
if (text != null) n.textContent = text; // textContent only: a record is never markup
|
||||
return n;
|
||||
}
|
||||
function show(nodes) {
|
||||
while (app.firstChild) app.removeChild(app.firstChild);
|
||||
var box = el("div", "panel");
|
||||
for (var i = 0; i < nodes.length; i++) box.appendChild(nodes[i]);
|
||||
app.appendChild(box);
|
||||
}
|
||||
function status(text, kind) {
|
||||
var p = el("p", "state" + (kind ? " state-" + kind : ""), text);
|
||||
p.setAttribute("role", "status");
|
||||
p.setAttribute("aria-live", "polite");
|
||||
return p;
|
||||
}
|
||||
function retryButton() {
|
||||
var b = el("button", "btn", "Try again");
|
||||
b.type = "button";
|
||||
b.addEventListener("click", load);
|
||||
return b;
|
||||
}
|
||||
function messageOf(bodyText) {
|
||||
try { return String((JSON.parse(bodyText) || {}).message || ""); }
|
||||
catch (e) { return ""; }
|
||||
}
|
||||
|
||||
// ---- the states -------------------------------------------------------------------
|
||||
function noCode() {
|
||||
show([
|
||||
status("This link is incomplete — it arrived without its code.", "warn"),
|
||||
el("p", "detail", "Open it again from the email that brought you here.")
|
||||
]);
|
||||
}
|
||||
// A MISSING WORKFLOW must not read as an expired invitation. This is our problem.
|
||||
function configError(httpStatus, msg) {
|
||||
show([
|
||||
status("The edit service is not available right now.", "warn"),
|
||||
el("p", "detail",
|
||||
"This is a problem at our end, not with your link. Nothing is wrong with your " +
|
||||
"listing. Please try again later, or reply to the email that brought you here."),
|
||||
el("p", "detail", "The service answered " + httpStatus + (msg ? " — " + msg : "") + ".")
|
||||
]);
|
||||
}
|
||||
// 404 / 500 "No item to return was found" — the hash is wrong or has expired.
|
||||
function invalidLink(httpStatus, msg) {
|
||||
show([
|
||||
status("This link is not valid, or it has expired.", "warn"),
|
||||
el("p", "detail", "The service answered " + httpStatus + (msg ? " — " + msg : "") + "."),
|
||||
el("p", "detail", "If you think that is wrong, reply to the email that brought you here.")
|
||||
]);
|
||||
}
|
||||
function loadFailed(httpStatus, msg) {
|
||||
show([
|
||||
status("We could not load your listing.", "warn"),
|
||||
el("p", "detail", "The service answered " + httpStatus + (msg ? " — " + msg : "") + "."),
|
||||
retryButton()
|
||||
]);
|
||||
}
|
||||
// Honest about a shape we did not expect: name the fields, never a value.
|
||||
function shapeMismatch(names) {
|
||||
show([
|
||||
status("We could not read your listing from the reply from the service.", "warn"),
|
||||
el("p", "detail",
|
||||
names.length
|
||||
? "The reply carried these fields: " + names.join(", ") + "."
|
||||
: "The reply carried no readable data."),
|
||||
retryButton()
|
||||
]);
|
||||
}
|
||||
|
||||
// ---- reading the record -----------------------------------------------------------
|
||||
function unwrap(payload) {
|
||||
if (Array.isArray(payload)) return payload[0] || null;
|
||||
if (payload && typeof payload === "object") {
|
||||
if (Array.isArray(payload.data)) return payload.data[0] || null;
|
||||
if (Array.isArray(payload.items)) return payload.items[0] || null;
|
||||
if (payload.data && typeof payload.data === "object") return payload.data;
|
||||
return payload;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
// Read one field by its list of accepted names. NEVER a value we did not expect:
|
||||
// anything not in C.aliases is unreadable.
|
||||
function readField(obj, key) {
|
||||
var names = C.aliases[key] || [key];
|
||||
for (var i = 0; i < names.length; i++) {
|
||||
var v = obj[names[i]];
|
||||
if (v !== undefined && v !== null && v !== "") return v;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
function hasAnyMappedField(obj) {
|
||||
for (var key in C.aliases) {
|
||||
if (key === "id") continue;
|
||||
if (readField(obj, key) !== undefined) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
function num(value, fallback) {
|
||||
var n = parseInt(value, 10);
|
||||
return isNaN(n) ? fallback : n;
|
||||
}
|
||||
function clamp05(n) { return Math.max(0, Math.min(C.focusMax, n)); }
|
||||
function initials(name) {
|
||||
var parts = String(name || "").trim().split(/\s+/).filter(Boolean);
|
||||
if (!parts.length) return "?";
|
||||
if (parts.length === 1) return parts[0].slice(0, 2).toUpperCase();
|
||||
return (parts[0][0] + parts[parts.length - 1][0]).toUpperCase();
|
||||
}
|
||||
|
||||
// ---- the form ---------------------------------------------------------------------
|
||||
function textField(id, labelText, value, maxLength, hint) {
|
||||
var wrap = el("div", "field");
|
||||
var label = el("label", "", labelText);
|
||||
label.setAttribute("for", id);
|
||||
var input = el("input");
|
||||
input.type = "text";
|
||||
input.id = id;
|
||||
input.name = id;
|
||||
input.value = value == null ? "" : String(value);
|
||||
if (maxLength) { input.maxLength = maxLength; input.setAttribute("aria-describedby", id + "Count"); }
|
||||
wrap.appendChild(label);
|
||||
wrap.appendChild(input);
|
||||
if (maxLength) {
|
||||
var counter = el("p", "counter", input.value.length + " / " + maxLength);
|
||||
counter.id = id + "Count";
|
||||
input.addEventListener("input", function () {
|
||||
var n = input.value.length;
|
||||
counter.textContent = n + " / " + maxLength;
|
||||
counter.setAttribute("data-over", n > maxLength ? "1" : "0");
|
||||
});
|
||||
counter.setAttribute("data-over", input.value.length > maxLength ? "1" : "0");
|
||||
wrap.appendChild(counter);
|
||||
}
|
||||
if (hint) wrap.appendChild(el("p", "hint", hint));
|
||||
return wrap;
|
||||
}
|
||||
|
||||
// An optional profile link. A real `type="url"` input with the application form's own
|
||||
// label and placeholder, so the two forms ask for a link the same way. No URL check
|
||||
// lives here: the value is passed through untouched, and the ONLY url predicate in the
|
||||
// fleet is the directory card's isLink() (a prefix allowlist) — one check, not two.
|
||||
function linkField(id, name, labelText, value, placeholder) {
|
||||
var wrap = el("div", "field");
|
||||
var label = el("label", "", labelText);
|
||||
label.setAttribute("for", id);
|
||||
label.appendChild(el("span", "hint", " (optional)"));
|
||||
var input = el("input");
|
||||
input.type = "url";
|
||||
input.id = id;
|
||||
input.name = name;
|
||||
input.value = value == null ? "" : String(value);
|
||||
input.placeholder = placeholder;
|
||||
wrap.appendChild(label);
|
||||
wrap.appendChild(input);
|
||||
return wrap;
|
||||
}
|
||||
|
||||
function rangeField(fieldset, name, labelText, value) {
|
||||
var row = el("div", "row");
|
||||
var label = el("label", "", labelText);
|
||||
label.setAttribute("for", "ed-" + name);
|
||||
var input = el("input");
|
||||
input.type = "range";
|
||||
input.id = "ed-" + name;
|
||||
input.name = name;
|
||||
input.min = "0";
|
||||
input.max = String(C.focusMax);
|
||||
input.step = "1";
|
||||
input.value = String(clamp05(num(value, 0)));
|
||||
input.setAttribute("aria-valuetext", input.value + " of " + C.focusMax);
|
||||
var out = el("output", "", input.value);
|
||||
out.setAttribute("for", "ed-" + name);
|
||||
input.addEventListener("input", function () {
|
||||
input.setAttribute("aria-valuetext", input.value + " of " + C.focusMax);
|
||||
out.textContent = input.value;
|
||||
updateTotal();
|
||||
});
|
||||
row.appendChild(label);
|
||||
row.appendChild(input);
|
||||
row.appendChild(out);
|
||||
fieldset.appendChild(row);
|
||||
}
|
||||
|
||||
function countrySelect(selected) {
|
||||
var sel = el("select");
|
||||
sel.id = "edCountry";
|
||||
sel.name = "Country";
|
||||
sel.appendChild(new Option("— choose —", ""));
|
||||
COUNTRIES.forEach(function (pair) {
|
||||
var o = new Option(pair[1], C.countryValueFormat === "name" ? pair[1] : pair[0]);
|
||||
var want = String(selected == null ? "" : selected).trim();
|
||||
if (want && (want.toUpperCase() === pair[0] || want === pair[1])) o.selected = true;
|
||||
sel.appendChild(o);
|
||||
});
|
||||
return sel;
|
||||
}
|
||||
|
||||
var focusTotalLine = null;
|
||||
function focusSum() {
|
||||
var sum = 0;
|
||||
C.focusAxes.forEach(function (axis) {
|
||||
var el2 = document.getElementById("ed-" + axis);
|
||||
sum += el2 ? num(el2.value, 0) : 0;
|
||||
});
|
||||
return sum;
|
||||
}
|
||||
function updateTotal() {
|
||||
if (!focusTotalLine) return;
|
||||
var sum = focusSum();
|
||||
focusTotalLine.textContent = sum + " of " + C.focusTotal + " points";
|
||||
focusTotalLine.setAttribute("data-bad", sum === C.focusTotal ? "0" : "1");
|
||||
}
|
||||
|
||||
function renderForm(record) {
|
||||
rec = record;
|
||||
var who = readField(record, "Name");
|
||||
var nodes = [];
|
||||
nodes.push(el("p", "who", "Listing: " + (who ? String(who) : "(no name)")));
|
||||
|
||||
var form = el("form", "edit-form");
|
||||
form.setAttribute("novalidate", "novalidate"); // JS announces errors instead
|
||||
|
||||
form.appendChild(textField("edName", "Name", readField(record, "Name"), 0, null));
|
||||
form.appendChild(textField("edFocus", "Focus", readField(record, "Focus"),
|
||||
C.focusMaxChars, "One short line — the kind of work you want, at most " + C.focusMaxChars + " characters."));
|
||||
|
||||
var descWrap = el("div", "field");
|
||||
var descLabel = el("label", "", "About you");
|
||||
descLabel.setAttribute("for", "edDescription");
|
||||
var ta = el("textarea");
|
||||
ta.id = "edDescription";
|
||||
ta.name = "edDescription";
|
||||
ta.rows = 6;
|
||||
ta.maxLength = C.descMaxChars;
|
||||
ta.value = String(readField(record, "description") == null ? "" : readField(record, "description"));
|
||||
ta.setAttribute("aria-describedby", "edDescriptionCount");
|
||||
var descCount = el("p", "counter", ta.value.length + " / " + C.descMaxChars);
|
||||
descCount.id = "edDescriptionCount";
|
||||
ta.addEventListener("input", function () {
|
||||
var n = ta.value.length;
|
||||
descCount.textContent = n + " / " + C.descMaxChars;
|
||||
descCount.setAttribute("data-over", n > C.descMaxChars ? "1" : "0");
|
||||
});
|
||||
descCount.setAttribute("data-over", ta.value.length > C.descMaxChars ? "1" : "0");
|
||||
descWrap.appendChild(descLabel);
|
||||
descWrap.appendChild(ta);
|
||||
descWrap.appendChild(descCount);
|
||||
form.appendChild(descWrap);
|
||||
|
||||
// The five axes. An axis the record does not carry keeps the application form's
|
||||
// default of 3, so the block always totals 15 and can be saved.
|
||||
var axesSet = el("fieldset", "");
|
||||
axesSet.appendChild(el("legend", "", CG.LEGEND));
|
||||
// The same directive the application form carries, so both forms state the rule.
|
||||
axesSet.appendChild(el("p", "hint", CG.DIRECTIVE));
|
||||
C.focusAxes.forEach(function (axis) {
|
||||
var v = readField(record, axis);
|
||||
if (v === undefined || v === null || v === "") v = 3; // → the block totals 15
|
||||
rangeField(axesSet, axis, C.focusAxisLabels[axis] || axis, v);
|
||||
});
|
||||
form.appendChild(axesSet);
|
||||
focusTotalLine = el("p", "total", "");
|
||||
focusTotalLine.id = "edFocusTotal";
|
||||
focusTotalLine.setAttribute("role", "status");
|
||||
focusTotalLine.setAttribute("aria-live", "polite");
|
||||
form.appendChild(focusTotalLine);
|
||||
|
||||
var langsSet = el("fieldset", "");
|
||||
langsSet.appendChild(el("legend", "", "Languages (0 = do not use it)"));
|
||||
C.langs.forEach(function (pair) {
|
||||
rangeField(langsSet, pair[0], pair[1], readField(record, pair[0]));
|
||||
});
|
||||
form.appendChild(langsSet);
|
||||
|
||||
var countryWrap = el("div", "field");
|
||||
var countryLabel = el("label", "", "Country");
|
||||
countryLabel.setAttribute("for", "edCountry");
|
||||
countryWrap.appendChild(countryLabel);
|
||||
countryWrap.appendChild(countrySelect(readField(record, "Country")));
|
||||
form.appendChild(countryWrap);
|
||||
|
||||
form.appendChild(textField("edOcaLink", "OCA member profile link", readField(record, "OCA_link"),
|
||||
0, "Your OCA profile URL, if you have one: https://apps.odoo-community.org/community/…"));
|
||||
|
||||
// The four profile links — parity with the application form. Keys are the LIVE wire
|
||||
// keys verbatim ("Linkedin" capital L; "github", "upwork" and "fiverr" lower) and the
|
||||
// labels and placeholders are the application form's own. Each is optional and
|
||||
// pre-filled from the record.
|
||||
form.appendChild(linkField("edLinkedin", "Linkedin", "LinkedIn URL",
|
||||
readField(record, "Linkedin"), "https://www.linkedin.com/in/…"));
|
||||
form.appendChild(linkField("edGithub", "github", "GitHub URL",
|
||||
readField(record, "github"), "https://github.com/…"));
|
||||
form.appendChild(linkField("edUpwork", "upwork", "Upwork URL",
|
||||
readField(record, "upwork"), "https://www.upwork.com/freelancers/…"));
|
||||
form.appendChild(linkField("edFiverr", "fiverr", "Fiverr URL",
|
||||
readField(record, "fiverr"), "https://www.fiverr.com/…"));
|
||||
|
||||
// The photo block is a LOOK, not a field. The photo does not live in this record
|
||||
// any more — the payload carries no `Image` — it comes from its own endpoint,
|
||||
// addressed by the record id (window.O4PExpertImage). So nothing here edits it,
|
||||
// and the save below sends no `Image` key at all: not the value, not null, not "".
|
||||
var photoWrap = el("div", "field");
|
||||
photoWrap.appendChild(el("label", "", "Photo"));
|
||||
var photoRow = el("div", "photo-readonly");
|
||||
var photoMono = el("span", "mono", initials(who));
|
||||
photoRow.appendChild(photoMono);
|
||||
if (rec && rec.id !== undefined && rec.id !== null && rec.id !== "") {
|
||||
// Same fetch the directory card makes: the route answers JSON, not image bytes,
|
||||
// so the value is read from `Image` and only then put on an <img>. Any failure
|
||||
// (or a value that fails isImageSrc) leaves the monogram standing.
|
||||
O4PExpertImage.load(String(rec.id)).then(function (src) {
|
||||
if (!src) return;
|
||||
var photoImg = el("img");
|
||||
photoImg.id = "edImagePreview";
|
||||
photoImg.alt = "";
|
||||
photoImg.decoding = "async";
|
||||
photoImg.addEventListener("load", function () {
|
||||
if (photoMono.parentNode) photoMono.parentNode.replaceChild(photoImg, photoMono);
|
||||
}, { once: true });
|
||||
photoImg.addEventListener("error", function () {
|
||||
// A failing photo is the monogram: no broken-image icon, no empty box.
|
||||
if (photoImg.parentNode) photoImg.parentNode.removeChild(photoImg);
|
||||
}, { once: true });
|
||||
photoImg.src = src;
|
||||
});
|
||||
}
|
||||
photoWrap.appendChild(photoRow);
|
||||
photoWrap.appendChild(el("p", "hint",
|
||||
"Your photo comes from your listing and is not editable here — reply to the email " +
|
||||
"that brought you here if it needs to change."));
|
||||
form.appendChild(photoWrap);
|
||||
|
||||
var save = el("button", "btn", "Save my changes");
|
||||
save.type = "submit";
|
||||
var line = el("p", "status", "");
|
||||
line.id = "editStatus";
|
||||
line.setAttribute("role", "status");
|
||||
line.setAttribute("aria-live", "polite");
|
||||
|
||||
form.addEventListener("submit", function (event) {
|
||||
event.preventDefault();
|
||||
if (focusSum() !== C.focusTotal) {
|
||||
var sum = focusSum();
|
||||
line.className = "status state-err";
|
||||
line.textContent = CG.message(sum); // names the direction: remove / add / place all
|
||||
focusTotalLine.setAttribute("data-bad", "1");
|
||||
return;
|
||||
}
|
||||
// A save is authorised by the HASH, not by the id. If this page holds no hash there
|
||||
// is nothing to prove ownership, so the request is not fired at all — a PATCH
|
||||
// carrying only an id is exactly the hole this closes. Say why, do not fail silently.
|
||||
if (!hash) {
|
||||
line.className = "status state-err";
|
||||
line.textContent = "This link arrived without its code, so we cannot save your " +
|
||||
"changes. Open it again from the email that brought you here.";
|
||||
return;
|
||||
}
|
||||
save.disabled = true;
|
||||
line.className = "status";
|
||||
line.textContent = "Saving…";
|
||||
// The body shape: READ-MODIFY-WRITE. Start from the record the webhook returned and
|
||||
// overwrite ONLY the fields this form owns. A field-by-field payload is what deletes a
|
||||
// freelancer's own photo and links on save — a partial payload wipes what it leaves
|
||||
// out, so the whole record goes back and only the edited keys are replaced. The
|
||||
// credential rides first: "has" is the SAME key the delete sends (one credential path
|
||||
// for both), then the record's id. Method is C.saveMethod ("PATCH").
|
||||
var payload = { has: hash };
|
||||
if (rec) {
|
||||
Object.keys(rec).forEach(function (k) {
|
||||
if (k === "has") return; // never let the record's own "has" shadow the credential
|
||||
payload[k] = rec[k]; // Image, the links, createdAt/updatedAt and everything else
|
||||
});
|
||||
}
|
||||
payload.id = rec && rec.id !== undefined ? rec.id : undefined;
|
||||
payload.Name = document.getElementById("edName").value.trim();
|
||||
payload.Focus = document.getElementById("edFocus").value.trim();
|
||||
payload.description = document.getElementById("edDescription").value.trim();
|
||||
payload.Country = document.getElementById("edCountry").value;
|
||||
payload.OCA_link = document.getElementById("edOcaLink").value.trim();
|
||||
C.focusAxes.forEach(function (axis) { payload[axis] = num(document.getElementById("ed-" + axis).value, 0); });
|
||||
C.langs.forEach(function (pair) { payload[pair[0]] = num(document.getElementById("ed-" + pair[0]).value, 0); });
|
||||
// The four links, read from their inputs. Only a key we actually have a value for is
|
||||
// touched: an empty optional field must NOT invent an empty string on a record that
|
||||
// never had one — the record's own value (null, or absent) is already in the payload
|
||||
// and is left as it is. A link the record DID carry and the owner cleared is a
|
||||
// deliberate clear and sends "".
|
||||
[["edLinkedin", "Linkedin"], ["edGithub", "github"], ["edUpwork", "upwork"], ["edFiverr", "fiverr"]].forEach(function (pair) {
|
||||
var input = document.getElementById(pair[0]);
|
||||
if (!input) return;
|
||||
var v = input.value.trim();
|
||||
var had = rec && rec[pair[1]] !== undefined && rec[pair[1]] !== null && rec[pair[1]] !== "";
|
||||
if (v) payload[pair[1]] = v;
|
||||
else if (had) payload[pair[1]] = "";
|
||||
});
|
||||
|
||||
// The photo does NOT ride in this record any more: the payload carries no
|
||||
// `Image` key and this form sends none — not the value, not null, not "". The
|
||||
// delete is the guarantee, whatever a reply may one day carry.
|
||||
delete payload.Image;
|
||||
|
||||
fetch(C.webhook, {
|
||||
method: C.saveMethod,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(payload)
|
||||
})
|
||||
.then(function (res) {
|
||||
if (!res.ok) throw new Error("HTTP " + res.status);
|
||||
// A RECEIPT, not a delivery claim: n8n answers 200 before the workflow runs.
|
||||
line.className = "status state-ok";
|
||||
line.textContent = "Saved — we have your changes. They go live once confirmed.";
|
||||
})
|
||||
.catch(function () {
|
||||
// Non-2xx or network: never a receipt; the data stays in the form.
|
||||
line.className = "status state-err";
|
||||
line.textContent = "We could not save your changes just now. Your edits are still " +
|
||||
"in the form — please try again, or reply to the email that brought you here.";
|
||||
})
|
||||
.then(function () { save.disabled = false; });
|
||||
});
|
||||
|
||||
form.appendChild(save);
|
||||
form.appendChild(line);
|
||||
nodes.push(form);
|
||||
nodes.push(deleteBlock());
|
||||
show(nodes);
|
||||
// The total needs the fields in the document to sum them — do it after show().
|
||||
updateTotal();
|
||||
}
|
||||
|
||||
// ---- delete -----------------------------------------------------------------------
|
||||
function deleteBlock() {
|
||||
var box = el("div", "danger");
|
||||
box.appendChild(el("h2", "", "Delete this listing"));
|
||||
box.appendChild(el("p", "",
|
||||
"This removes your public listing for good. It cannot be undone from our side. " +
|
||||
"Tick the box and confirm — nothing is deleted until you do."));
|
||||
var confirmWrap = el("label", "confirm");
|
||||
var box_in = el("input");
|
||||
box_in.type = "checkbox";
|
||||
box_in.id = "edConfirmDelete";
|
||||
confirmWrap.appendChild(box_in);
|
||||
confirmWrap.appendChild(el("span", "",
|
||||
"Yes, delete my listing permanently."));
|
||||
box.appendChild(confirmWrap);
|
||||
|
||||
var del = el("button", "btn btn-danger", "Delete my listing");
|
||||
del.type = "button";
|
||||
del.disabled = true;
|
||||
var line = el("p", "status", "");
|
||||
line.id = "deleteStatus";
|
||||
line.setAttribute("role", "status");
|
||||
line.setAttribute("aria-live", "polite");
|
||||
|
||||
// The deliberate second action: the button cannot fire until the box is ticked.
|
||||
box_in.addEventListener("change", function () { del.disabled = !box_in.checked; });
|
||||
|
||||
del.addEventListener("click", function () {
|
||||
if (!box_in.checked) return;
|
||||
del.disabled = true;
|
||||
box_in.disabled = true;
|
||||
line.className = "status";
|
||||
line.textContent = "Deleting…";
|
||||
// DELETE with the credential and the record id. C.deleteMethod is the one place
|
||||
// the method lives; the id wire key is the record's own "id".
|
||||
fetch(C.webhook, {
|
||||
method: C.deleteMethod,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ has: hash, id: rec && rec.id !== undefined ? rec.id : null })
|
||||
})
|
||||
.then(function (res) {
|
||||
if (!res.ok) throw new Error("HTTP " + res.status);
|
||||
// Only a 2xx is a success. Anything else fails below.
|
||||
line.className = "status state-ok";
|
||||
line.textContent = "Your listing has been deleted. Thank you for having been listed.";
|
||||
del.hidden = true;
|
||||
confirmWrap.hidden = true;
|
||||
})
|
||||
.catch(function () {
|
||||
box_in.disabled = false;
|
||||
del.disabled = !box_in.checked;
|
||||
line.className = "status state-err";
|
||||
line.textContent = "We could not delete your listing just now — nothing was removed. " +
|
||||
"Please try again, or reply to the email that brought you here.";
|
||||
});
|
||||
});
|
||||
|
||||
box.appendChild(del);
|
||||
box.appendChild(line);
|
||||
return box;
|
||||
}
|
||||
|
||||
// ---- the GET ----------------------------------------------------------------------
|
||||
function load() {
|
||||
if (!hash) { noCode(); return; }
|
||||
show([status("Loading your listing…")]);
|
||||
|
||||
fetch(C.webhook + "?" + encodeURIComponent(C.hashWireKey) + "=" + encodeURIComponent(hash), {
|
||||
headers: { Accept: "application/json" }
|
||||
})
|
||||
.then(function (res) {
|
||||
if (!res.ok) {
|
||||
return res.text().then(function (body) {
|
||||
var msg = messageOf(body);
|
||||
// A missing workflow is a CONFIGURATION error — never "your link is invalid".
|
||||
if (/is not registered/i.test(msg)) { configError(res.status, msg); return; }
|
||||
if (res.status === 404 || res.status === 500) { invalidLink(res.status, msg); return; }
|
||||
loadFailed(res.status, msg);
|
||||
});
|
||||
}
|
||||
return res.json().then(function (payload) {
|
||||
var obj = unwrap(payload);
|
||||
if (!obj) { shapeMismatch([]); return; }
|
||||
if (!hasAnyMappedField(obj)) { shapeMismatch(Object.keys(obj)); return; }
|
||||
renderForm(obj);
|
||||
}).catch(function () {
|
||||
shapeMismatch([]);
|
||||
});
|
||||
})
|
||||
.catch(function () {
|
||||
// Network failure: the hash is still in memory, so a retry needs no new link.
|
||||
show([
|
||||
status("We could not load your listing just now.", "warn"),
|
||||
el("p", "detail", "Your link has been kept, so you can try again."),
|
||||
retryButton()
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
load();
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
Before Width: | Height: | Size: 158 KiB After Width: | Height: | Size: 155 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 19 KiB |
@@ -128,7 +128,9 @@
|
||||
.hero-stats{display:flex;gap:1.5rem;margin-bottom:2rem;flex-wrap:wrap}.hero-stat{display:flex;align-items:center;gap:.4rem;color:rgba(255,255,255,.65);font-size:.85rem}.hero-stat i{color:var(--brass);font-size:.75rem}
|
||||
.hero-actions{display:flex;gap:1rem;flex-wrap:wrap;margin-bottom:2rem}
|
||||
.hero-notice{position:absolute;top:1.25rem;right:1.25rem;z-index:3;display:flex;align-items:center;gap:.55rem;max-width:min(360px,calc(100% - 2rem));padding:.6rem .85rem .6rem 1rem;background:#f5c542;color:var(--midnight);border:1px solid rgba(13,27,46,.14);border-radius:var(--radius);box-shadow:0 8px 24px rgba(13,27,46,.3);font-family:Inter,sans-serif;font-size:.82rem;line-height:1.4;animation:heroNoticeIn .4s cubic-bezier(.16,1,.3,1)}
|
||||
.hero-notice[hidden]{display:none}
|
||||
/* `.hero-notice[hidden]{display:none}` used to live here; the site-wide `[hidden]`
|
||||
rule in /style.css hides it (`.hero-notice` sets `display:flex`, which outranked
|
||||
the UA rule). */
|
||||
.hero-notice i{color:var(--midnight);font-size:.9rem;flex-shrink:0}
|
||||
.hero-notice-text{flex:1;min-width:0;overflow-wrap:anywhere}
|
||||
.hero-notice-close{flex-shrink:0;display:flex;align-items:center;justify-content:center;width:1.35rem;height:1.35rem;padding:0;border:0;border-radius:50%;background:rgba(13,27,46,.12);color:var(--midnight);font-size:1rem;line-height:1;cursor:pointer;transition:background .2s}
|
||||
@@ -219,16 +221,25 @@
|
||||
#customQuoteModal .biz-success h3{color:var(--midnight)}
|
||||
#customQuoteModal .biz-success p{color:var(--midnight)}
|
||||
@media(max-width:600px){.oca-table td,.oca-table th{padding:.5rem .65rem;font-size:.8rem}.ow-panel{left:5%;right:5%}.ow-close{top:.5rem;right:.5rem;font-size:1.6rem}}
|
||||
/* Black Friday 2026 — seasonal top banner. Live 2-30 November 2026 only: remove this rule
|
||||
together with the .bf-topbar markup block at the top of <body> after 30 November 2026.
|
||||
CI: midnight canvas, brass label/code/CTA only, cream-white body text. No script, no tag. */
|
||||
.bf-topbar{display:flex;align-items:center;justify-content:center;gap:.3rem 1rem;flex-wrap:wrap;background:var(--midnight);border-bottom:1px solid var(--line-brass);padding:.65rem 1.25rem;font-family:Inter,sans-serif;font-size:.82rem;line-height:1.5;color:rgba(255,255,255,.82);text-align:center;text-decoration:none}
|
||||
.bf-topbar:hover,.bf-topbar:focus-visible{background:var(--midnight-2);color:#fff}
|
||||
.bf-topbar-badge{font-family:Marcellus,Georgia,serif;font-size:.78rem;letter-spacing:.08em;text-transform:uppercase;color:var(--brass);white-space:nowrap}
|
||||
.bf-topbar strong{color:#fff;font-weight:600}
|
||||
.bf-topbar-code{font-family:Marcellus,Georgia,serif;letter-spacing:.06em;color:var(--brass-bright)}
|
||||
.bf-topbar-cta{color:var(--brass);white-space:nowrap}
|
||||
.bf-topbar:hover .bf-topbar-cta,.bf-topbar:focus-visible .bf-topbar-cta{color:var(--brass-bright)}
|
||||
</style>
|
||||
<link rel="stylesheet" href="style.css" media="print" onload="this.media='all'" />
|
||||
<noscript><link rel="stylesheet" href="style.css" /></noscript>
|
||||
<link rel="stylesheet" href="style.css?v=061517baef" />
|
||||
<!-- Privacy-friendly analytics by Plausible -->
|
||||
<script async src="https://plausible.office.odoo4projects.com/js/pa-NhA7Rf-79VopXFQXhSS4e.js"></script>
|
||||
<script>
|
||||
window.plausible=window.plausible||function(){(plausible.q=plausible.q||[]).push(arguments)},plausible.init=plausible.init||function(i){plausible.o=i||{}};
|
||||
plausible.init()
|
||||
</script>
|
||||
<script defer src="/static/lang.js"></script>
|
||||
<script defer src="/static/lang.js?v=3ec49a7e62"></script>
|
||||
</head>
|
||||
<body>
|
||||
<!--
|
||||
@@ -239,7 +250,18 @@
|
||||
FORM: The Grand Hotel Business Lounge (model-pick, user-chosen). Seed key: 7db4dced.
|
||||
FINISH: unreviewed and undocumented is unfinished; this build ends with the finish review, the verdict, and DESIGN.md.
|
||||
-->
|
||||
<!-- Black Friday 2026 — seasonal banner. Live 2-30 November 2026 only: remove this whole block
|
||||
(and the .bf-topbar rules in <style>) after 30 November 2026. It is one link to the deal page:
|
||||
no script, no tracking tag, no event. The offer itself lives on /odoo-black-friday/ only. -->
|
||||
<a class="bf-topbar" href="/odoo-black-friday/">
|
||||
<span class="bf-topbar-badge">Black Friday</span>
|
||||
<span><strong>$100 off your first year</strong> · On the Rise: $395 a year list → $295 for your first year</span>
|
||||
<span>Code <span class="bf-topbar-code">HOST27</span> · Ends 30 November 2026</span>
|
||||
<span class="bf-topbar-cta">See the offer →</span>
|
||||
</a>
|
||||
|
||||
<!-- Hero -->
|
||||
<main>
|
||||
<section class="hero">
|
||||
<div class="hero-notice" id="heroConfirmNotice" role="status" aria-live="polite" hidden>
|
||||
<i class="fas fa-check-circle" aria-hidden="true"></i>
|
||||
@@ -390,6 +412,10 @@
|
||||
<div class="faq-q"><span>Do you have an affiliate program?</span> <i class="fas fa-chevron-down"></i></div>
|
||||
<div class="faq-a"><p>No. We believe it is in our best interest to have competitive prices for our shared clients.</p></div>
|
||||
</div>
|
||||
<div class="faq-card plausible-event-name=FAQ" onclick="this.classList.toggle('open')">
|
||||
<div class="faq-q"><span>I need a developer for a custom module — where do I find one?</span> <i class="fas fa-chevron-down"></i></div>
|
||||
<div class="faq-a"><p>We keep a directory of independent Odoo freelancers. Each card carries what the freelancer published and their contact details where they published them. <a href="odoo-community-developers/">Browse the Odoo freelancers</a>.</p></div>
|
||||
</div>
|
||||
</div>
|
||||
<a href="support.html" class="faq-link">Full FAQ →</a>
|
||||
</div>
|
||||
@@ -499,7 +525,7 @@
|
||||
<h2 class="ow-title" id="orderPlanTitle">On the Rise</h2>
|
||||
<p class="ow-price">
|
||||
<span class="ow-price-amount" id="orderPlanPrice">$ 395.00</span> <span id="orderPlanPeriod">/ year</span>
|
||||
<span class="ow-price-tax" id="orderPlanTax">+ tax</span>
|
||||
<span class="ow-price-tax" id="orderPlanTax">tax included</span>
|
||||
</p>
|
||||
<p class="ow-coupon-msg" id="orderCouponMsg">Questions before you buy? Ask the assistant on this page.</p>
|
||||
<p class="ow-yearly-save" id="orderYearlySave">Billed yearly. No setup fee.</p>
|
||||
@@ -629,6 +655,7 @@
|
||||
</section>
|
||||
|
||||
<!-- Footer -->
|
||||
</main>
|
||||
<footer class="footer">
|
||||
<div class="container">
|
||||
<div class="fgrid">
|
||||
@@ -637,6 +664,7 @@
|
||||
<ul>
|
||||
<li><a href="/odoo-erp-automation-bootcamp/">ODOO Bootcamp</a></li>
|
||||
<li><a href="odoo-hosting-2027.html">ODOO Hosting in 2027</a></li>
|
||||
<li><a href="/odoo-community-developers/">Odoo Freelancer Directory</a></li>
|
||||
<li><a href="support.html">Support</a></li>
|
||||
<li><a href="legal.html">Legal</a></li>
|
||||
</ul>
|
||||
@@ -675,7 +703,7 @@
|
||||
</footer>
|
||||
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.5/gsap.min.js"></script>
|
||||
<script src="script.js"></script>
|
||||
<script defer src="/static/agent-odoo4projects/agent.js"></script>
|
||||
<script src="script.js?v=d2046e3469"></script>
|
||||
<script defer src="/static/agent-odoo4projects/agent.js?v=c3bb0cee2b"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -25,7 +25,7 @@
|
||||
rel="stylesheet"
|
||||
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css"
|
||||
/>
|
||||
<link rel="stylesheet" href="style.css" />
|
||||
<link rel="stylesheet" href="style.css?v=061517baef" />
|
||||
<style>
|
||||
.legal-hero {
|
||||
padding: 4rem 0 2.5rem;
|
||||
@@ -123,6 +123,7 @@
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<div class="container">
|
||||
<div class="blog-header">
|
||||
<a href="index.html">← Back to Home</a>
|
||||
@@ -145,7 +146,7 @@
|
||||
WY 82801, USA
|
||||
</p>
|
||||
<p style="margin-top: 0.75rem;">
|
||||
Contact: <span class="contact-email">support@ODOO4projects.com</span>
|
||||
Contact: <span class="contact-email">support@odoo4projects.com</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -202,6 +203,7 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<script defer src="/static/agent-odoo4projects/agent.js"></script>
|
||||
</main>
|
||||
<script defer src="/static/agent-odoo4projects/agent.js?v=c3bb0cee2b"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,9 +1,10 @@
|
||||
# ODOO4projects
|
||||
|
||||
> ODOO4projects is a managed Odoo Community hosting service. We run the server, the backups, the
|
||||
> SSL certificate and the version upgrades for a monthly-or-yearly price, so the customer does not
|
||||
> have to administer a server. One login: everything happens inside the customer's own Odoo
|
||||
> instance. We do not sell Odoo itself; we sell a business system that runs.
|
||||
> SSL certificate and the updates that keep your Odoo current within its version, for a monthly-or-
|
||||
> yearly price, so the customer does not have to administer a server. Moving to a new major version
|
||||
> is a migration and is quoted separately. One login: everything happens inside the customer's own
|
||||
> Odoo instance. We do not sell Odoo itself; we sell a business system that runs.
|
||||
|
||||
## Products and pricing (verified 2026-10-21, list prices, USD, VAT excluded)
|
||||
|
||||
@@ -37,9 +38,10 @@
|
||||
- [Odoo hosting 2027 — comparison landing page](https://odoo4projects.com/odoo-hosting-2027.html): every route to Odoo (managed, VPS, odoo.sh, Odoo Online, own hardware) side by side, with annual and five-year cost comparisons.
|
||||
- [Odoo ERP Automation Bootcamp](https://odoo4projects.com/odoo-erp-automation-bootcamp/): a three-month, ten-session live bootcamp with Oliver Arnold, Technical Lead, that takes a small business from zero to a running ERP. Thursdays, one hour, Q1 2027, starting 21 January 2027; one year of hosted Odoo Community Edition included. The bootcamp is limited to six participants per cohort. The current price is confirmed on registration.
|
||||
- [ODOO x N8N Masterclass](https://odoo4projects.com/odoo-n8n-masterclass/): a two-hour live masterclass with Oliver Arnold on connecting Odoo to n8n — calling n8n webhooks from Odoo automated actions, how the n8n integration for Odoo works, and website workarounds that extend your automation. Hands-on in a free Odoo trial system; n8n itself runs on n8n's own free plan, which is not included. USD 120.00, next session the four Thursdays of February 2027 at 17:00 Central Time.
|
||||
- [ODOO Workshops and Classes](https://odoo4projects.com/workshops/): the two live courses side by side — the two-hour ODOO x N8N Masterclass (USD 120.00) and the ten-session ODOO ERP Automation Bootcamp (USD 465.00), both taught by Oliver Arnold — which course fits which business, and a full comparison table.
|
||||
- [ODOO Workshops and Classes](https://odoo4projects.com/workshops/): the two live courses side by side — the two-hour ODOO x N8N Masterclass (USD 120.00) and the ten-session ODOO ERP Automation Bootcamp (USD 595.00), both taught by Oliver Arnold — which course fits which business, and a full comparison table.
|
||||
- [What can I use Odoo for?](https://odoo4projects.com/what-can-i-use-odoo-for/): plain-English guide to what Odoo is used for — the ten apps and what each is for, what Odoo is used for by business type (manufacturing, retail and trade, services, construction, nonprofits, accounting practices, small business starting out), where it stops (Odoo Online will not take custom code, it will not do your trade's specialist work), what it costs, and how to start. Includes the nine-question FAQ.
|
||||
- [Odoo staging environment](https://odoo4projects.com/odoo-staging-environment/): what an Odoo staging environment is, how odoo.sh's staging branches work, why most small businesses do not need one, and the simpler back-up-install-test route we offer instead.
|
||||
- [Odoo freelancers](https://odoo4projects.com/odoo-community-developers/): independent Odoo freelancers for our clients looking for one. Each card carries what the freelancer published and their contact details where they published them — no rating and no ranking, and the client writes to the freelancer directly.
|
||||
- [Home](https://odoo4projects.com/): plans and pricing, FAQ, server locations.
|
||||
- [Support](https://odoo4projects.com/support.html): support and frequently asked questions.
|
||||
- [Legal](https://odoo4projects.com/legal.html): company details.
|
||||
@@ -48,6 +50,10 @@
|
||||
|
||||
## Blog posts
|
||||
|
||||
- [Your Agent Can Run Your Odoo Hosting: the MCP Server Is Live](https://odoo4projects.com/blog/posts/your-agent-can-run-your-odoo-hosting.html) — 2026-11-05 — This is the piece of the Odoo 20 update we are asked about most: an MCP server that lets your agent work on your Odoo hosting directly, instead of you clicking through menus to do routine things.
|
||||
- [Our freelancer directory opens on 1 December. Help us get the signup right](https://odoo4projects.com/blog/posts/freelancer-directory-soft-launch.html) — 2026-11-04 — We need your help! We are opening our Odoo Community Freelancer directory on 1 December. Before real clients start browsing it, we want the signup to work, and we would like your help to get it right.
|
||||
- [Odoo 20 Is Live on Our Servers — Here Is What Changed](https://odoo4projects.com/blog/posts/odoo-20-is-live-on-our-servers-here-is-what-changed.html) — 2026-11-03 — Two weeks. That is how long it took us to bring up Odoo 20 on our servers, with every new feature that comes with it. It is live now — if your instance runs with us, the engine underneath it just moved forward.
|
||||
- [From a Spreadsheet to a Running ERP in Ten Thursday Sessions](https://odoo4projects.com/blog/posts/from-a-spreadsheet-to-a-running-erp.html) — 2026-11-02 — Most small businesses do not run on an ERP. They run on a spreadsheet, a messaging group, and one person's memory. It works — until that person is on holiday.
|
||||
- [ODOO4projects Supports the OCA Paraguay Internationalization Hackathon](https://odoo4projects.com/blog/posts/oca-paraguay-internationalization-hackathon.html) — 2026-10-29 — Two online days to push Odoo's Paraguayan localisation forward — and we are supporting them.
|
||||
- [How to Install Odoo Modules Using Git — and the Quick Git Route That Skips the Console](https://odoo4projects.com/blog/posts/how-to-install-odoo-modules-using-git.html) — 2026-10-28 — Every business is slightly different, which is why a system that only does the standard thing is only ever half a system.
|
||||
- [ODOO Community Is Going Online on ODOO4projects](https://odoo4projects.com/blog/posts/odoo-community-is-going-online-on-odoo4projects.html) — 2026-10-27 — ODOO Community is online.
|
||||
|
||||
@@ -23,8 +23,7 @@
|
||||
onload="this.media='all'"
|
||||
/>
|
||||
<noscript><link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Marcellus&display=swap" /></noscript>
|
||||
<link rel="stylesheet" href="/style.css" media="print" onload="this.media='all'" />
|
||||
<noscript><link rel="stylesheet" href="/style.css" /></noscript>
|
||||
<link rel="stylesheet" href="/style.css?v=061517baef" />
|
||||
<style>
|
||||
/* ---- "Odoo Community + AI agent" page (page-scoped; additive).
|
||||
Reuses the top bar, band rhythm, hero, FAQ and final-CTA treatment already
|
||||
@@ -662,6 +661,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Hero -->
|
||||
<main>
|
||||
<section class="hero oc-hero">
|
||||
<div class="hero-bg">
|
||||
<img
|
||||
@@ -889,6 +889,7 @@
|
||||
</section>
|
||||
|
||||
<!-- Footer (copied from odoo-staging-environment/; no link to this page is added anywhere) -->
|
||||
</main>
|
||||
<footer class="footer">
|
||||
<div class="container">
|
||||
<div class="fgrid">
|
||||
@@ -897,6 +898,7 @@
|
||||
<ul>
|
||||
<li><a href="/odoo-erp-automation-bootcamp/">ODOO Bootcamp</a></li>
|
||||
<li><a href="/odoo-hosting-2027.html">ODOO Hosting in 2027</a></li>
|
||||
<li><a href="/odoo-community-developers/">Odoo Freelancer Directory</a></li>
|
||||
<li><a href="/support.html">Support</a></li>
|
||||
<li><a href="/legal.html">Legal</a></li>
|
||||
</ul>
|
||||
@@ -920,7 +922,7 @@
|
||||
<a href="https://www.linkedin.com/company/odoo4projects/" target="_blank" aria-label="LinkedIn"><svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M4.98 3.5a2.5 2.5 0 1 1 0 5 2.5 2.5 0 0 1 0-5zM3 9h4v12H3zM9 9h3.8v1.7h.05c.53-.95 1.83-1.95 3.76-1.95 4.02 0 4.76 2.5 4.76 5.75V21h-4v-5.5c0-1.31-.02-3-1.9-3-1.9 0-2.19 1.42-2.19 2.9V21H9z" /></svg></a>
|
||||
<a href="https://www.youtube.com/@ODOO4projects" target="_blank" aria-label="YouTube"><svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M23 12s0-3.2-.41-4.73a2.5 2.5 0 0 0-1.76-1.77C19.3 5.1 12 5.1 12 5.1s-7.3 0-8.83.4A2.5 2.5 0 0 0 1.4 7.27C1 8.8 1 12 1 12s0 3.2.41 4.73a2.5 2.5 0 0 0 1.76 1.77c1.53.4 8.83.4 8.83.4s7.3 0 8.83-.4a2.5 2.5 0 0 0 1.76-1.77C23 15.2 23 12 23 12zM9.75 15.02V8.98L15.5 12z" /></svg></a>
|
||||
</div>
|
||||
<form class="footer-contact-form" id="footerContactForm" action="mailto:hello@odoo4projects.com" method="post" enctype="text/plain">
|
||||
<form class="footer-contact-form" id="footerContactForm" action="mailto:support@odoo4projects.com" method="post" enctype="text/plain">
|
||||
<label class="oca-sr-only" for="footerContactEmail">Your email address</label>
|
||||
<input type="email" id="footerContactEmail" name="email" placeholder="Your email address" required />
|
||||
<div class="footer-textarea-wrap">
|
||||
@@ -935,6 +937,6 @@
|
||||
<div class="fcopy">Copyright © ODOO4projects LLC</div>
|
||||
</div>
|
||||
</footer>
|
||||
<script defer src="/static/lang.js"></script>
|
||||
<script defer src="/static/lang.js?v=3ec49a7e62"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -38,8 +38,7 @@
|
||||
onload="this.media='all'"
|
||||
/>
|
||||
<noscript><link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Marcellus&display=swap" /></noscript>
|
||||
<link rel="stylesheet" href="style.css" media="print" onload="this.media='all'" />
|
||||
<noscript><link rel="stylesheet" href="style.css" /></noscript>
|
||||
<link rel="stylesheet" href="style.css?v=061517baef" />
|
||||
<style>
|
||||
/* ---- Odoo Community Upgrade page (page-scoped; additive) ---- */
|
||||
.oc-sr-only {
|
||||
@@ -480,7 +479,7 @@
|
||||
window.plausible=window.plausible||function(){(plausible.q=plausible.q||[]).push(arguments)},plausible.init=plausible.init||function(i){plausible.o=i||{}};
|
||||
plausible.init()
|
||||
</script>
|
||||
<script defer src="/static/lang.js"></script>
|
||||
<script defer src="/static/lang.js?v=3ec49a7e62"></script>
|
||||
</head>
|
||||
<body>
|
||||
<!-- Page-scoped top bar: the site has no global header, so the page carries its own way home. -->
|
||||
@@ -496,6 +495,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Hero -->
|
||||
<main>
|
||||
<section class="hero oc-hero">
|
||||
<div class="hero-bg">
|
||||
<img
|
||||
@@ -987,6 +987,7 @@ sudo -u postgres pg_restore -d <DB_NEW> /path/backup/<DB>.dump
|
||||
|
||||
<!-- Footer (copied from index.html; icon-font glyphs replaced with inline SVG so the page
|
||||
carries no external icon dependency) -->
|
||||
</main>
|
||||
<footer class="footer">
|
||||
<div class="container">
|
||||
<div class="fgrid">
|
||||
@@ -995,6 +996,7 @@ sudo -u postgres pg_restore -d <DB_NEW> /path/backup/<DB>.dump
|
||||
<ul>
|
||||
<li><a href="/odoo-erp-automation-bootcamp/">ODOO Bootcamp</a></li>
|
||||
<li><a href="odoo-hosting-2027.html">ODOO Hosting in 2027</a></li>
|
||||
<li><a href="/odoo-community-developers/">Odoo Freelancer Directory</a></li>
|
||||
<li><a href="support.html">Support</a></li>
|
||||
<li><a href="legal.html">Legal</a></li>
|
||||
</ul>
|
||||
@@ -1018,7 +1020,7 @@ sudo -u postgres pg_restore -d <DB_NEW> /path/backup/<DB>.dump
|
||||
<a href="https://www.linkedin.com/company/odoo4projects/" target="_blank" aria-label="LinkedIn"><svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M4.98 3.5a2.5 2.5 0 1 1 0 5 2.5 2.5 0 0 1 0-5zM3 9h4v12H3zM9 9h3.8v1.7h.05c.53-.95 1.83-1.95 3.76-1.95 4.02 0 4.76 2.5 4.76 5.75V21h-4v-5.5c0-1.31-.02-3-1.9-3-1.9 0-2.19 1.42-2.19 2.9V21H9z" /></svg></a>
|
||||
<a href="https://www.youtube.com/@ODOO4projects" target="_blank" aria-label="YouTube"><svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M23 12s0-3.2-.41-4.73a2.5 2.5 0 0 0-1.76-1.77C19.3 5.1 12 5.1 12 5.1s-7.3 0-8.83.4A2.5 2.5 0 0 0 1.4 7.27C1 8.8 1 12 1 12s0 3.2.41 4.73a2.5 2.5 0 0 0 1.76 1.77c1.53.4 8.83.4 8.83.4s7.3 0 8.83-.4a2.5 2.5 0 0 0 1.76-1.77C23 15.2 23 12 23 12zM9.75 15.02V8.98L15.5 12z" /></svg></a>
|
||||
</div>
|
||||
<form class="footer-contact-form" id="footerContactForm" action="mailto:hello@odoo4projects.com" method="post" enctype="text/plain">
|
||||
<form class="footer-contact-form" id="footerContactForm" action="mailto:support@odoo4projects.com" method="post" enctype="text/plain">
|
||||
<label class="oc-sr-only" for="footerContactEmail">Your email address</label>
|
||||
<input type="email" id="footerContactEmail" name="email" placeholder="Your email address" required />
|
||||
<div class="footer-textarea-wrap">
|
||||
|
||||
@@ -31,8 +31,9 @@
|
||||
onload="this.media='all'"
|
||||
/>
|
||||
<noscript><link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Marcellus&display=swap" /></noscript>
|
||||
<link rel="stylesheet" href="/style.css" media="print" onload="this.media='all'" />
|
||||
<noscript><link rel="stylesheet" href="/style.css" /></noscript>
|
||||
<link rel="preload" as="image" href="/images/hero/bootcamp-hero-mobile-900.webp" media="(max-width: 640px)" fetchpriority="high" />
|
||||
<link rel="preload" as="image" href="/images/hero/bootcamp-hero-desktop-1920.webp" media="(min-width: 641px)" fetchpriority="high" />
|
||||
<link rel="stylesheet" href="/style.css?v=061517baef" />
|
||||
<style>
|
||||
/* ---- ODOO ERP Automation Bootcamp landing page (page-scoped; additive) ---- */
|
||||
.bc-sr-only {
|
||||
@@ -192,6 +193,14 @@
|
||||
form[action$="/shop/post_basket"] .btn {
|
||||
line-height: inherit;
|
||||
}
|
||||
/* At 0 seats /static/bootcamp-availability.js disables these Buy buttons; make the
|
||||
disabled state read as disabled — quiet dimming in the page's own CI, never an
|
||||
alert banner. */
|
||||
form[action$="/shop/post_basket"] .btn:disabled,
|
||||
form[action$="/shop/post_basket"] .btn[aria-disabled="true"] {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* Grids — auto-fit so no breakpoint is needed */
|
||||
.bc-list {
|
||||
@@ -223,7 +232,7 @@
|
||||
border-radius: var(--radius-lg);
|
||||
box-shadow: var(--shadow-rest);
|
||||
padding: 1.4rem 1.4rem 1.4rem 3.4rem;
|
||||
transition: box-shadow 0.25s, transform 0.25s;
|
||||
transition: transform 0.25s; /* box-shadow kept, not transitioned (non-composited) */
|
||||
}
|
||||
.bc-lessons li:hover {
|
||||
box-shadow: var(--shadow-hover);
|
||||
@@ -313,7 +322,7 @@
|
||||
.bc-schedule dt {
|
||||
font-family: Marcellus, Georgia, serif;
|
||||
font-size: 0.92rem;
|
||||
color: var(--brass-deep);
|
||||
color: #876c1e; /* 4.68:1 on --ivory (was --brass-deep 4.49:1) */
|
||||
}
|
||||
.bc-schedule dd {
|
||||
margin: 0;
|
||||
@@ -452,6 +461,109 @@
|
||||
.btn-brass:focus-visible {
|
||||
outline-color: #fff;
|
||||
}
|
||||
/* Waiting list — for someone who cannot attend the January cohort.
|
||||
Deliberately quieter than the Buy button: outline button, smaller type, no brass fill. */
|
||||
.bc-waitlist {
|
||||
/* Component-scoped error colour: the site has no shared token for it. */
|
||||
--wl-error: #b02a37;
|
||||
max-width: 720px;
|
||||
margin: 2.5rem auto 0;
|
||||
padding: 1.5rem;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--radius);
|
||||
background: var(--ivory);
|
||||
text-align: left;
|
||||
}
|
||||
.bc-wl-title {
|
||||
margin: 0 0 0.5rem;
|
||||
font-size: 1.1rem;
|
||||
color: var(--ink);
|
||||
}
|
||||
.bc-wl-lead {
|
||||
margin: 0 0 1.1rem;
|
||||
font-size: 0.88rem;
|
||||
line-height: 1.6;
|
||||
color: rgba(23, 28, 38, 0.78); /* the page's own .bc-note value, on the same cream */
|
||||
}
|
||||
.bc-wl-form input,
|
||||
.bc-wl-form textarea {
|
||||
display: block;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
margin-bottom: 0.6rem;
|
||||
padding: 0.6rem 0.75rem;
|
||||
font-family: Inter, sans-serif;
|
||||
font-size: 0.9rem;
|
||||
color: var(--ink);
|
||||
background: #fff;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--radius);
|
||||
}
|
||||
.bc-wl-form textarea {
|
||||
min-height: 3.4rem;
|
||||
resize: vertical;
|
||||
}
|
||||
.bc-wl-btn {
|
||||
padding: 0.6rem 1.5rem;
|
||||
font-size: 0.8rem;
|
||||
background: transparent;
|
||||
color: var(--ink);
|
||||
border-color: rgba(23, 28, 38, 0.35);
|
||||
}
|
||||
.bc-wl-btn:hover {
|
||||
border-color: var(--brass);
|
||||
background: rgba(201, 162, 39, 0.12);
|
||||
transform: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
.bc-wl-btn:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: default;
|
||||
}
|
||||
.bc-wl-success {
|
||||
margin: 0;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
color: var(--success);
|
||||
}
|
||||
.bc-wl-error {
|
||||
margin: 0;
|
||||
font-size: 0.9rem;
|
||||
color: var(--wl-error);
|
||||
}
|
||||
|
||||
/* --- 2026-11-02 PageSpeed pass (page-scoped, additive) ---------------------
|
||||
1. The footer <h4>s became <h3>s so the heading order descends after the last
|
||||
section h2 (a11y "heading-order"); these rules keep their look identical to
|
||||
the shared .footer h4 in /style.css.
|
||||
2. A link inside an on-dark note is the lighter brass (7.15:1 on midnight,
|
||||
A11y "color-contrast") and underlined, so it is distinguishable without
|
||||
colour (A11y "link-in-text-block"). */
|
||||
.footer h3 {
|
||||
font-family: Marcellus, Georgia, serif;
|
||||
color: #fff;
|
||||
margin-bottom: 0.8rem;
|
||||
font-size: 1rem;
|
||||
letter-spacing: 0.03em;
|
||||
}
|
||||
.bc-note.on-dark a {
|
||||
color: var(--brass);
|
||||
text-decoration: underline;
|
||||
}
|
||||
/* the footer copyright line at .35 alpha was 3.2:1 on midnight */
|
||||
.footer .fcopy {
|
||||
color: rgba(255, 255, 255, 0.5); /* 5.17:1 */
|
||||
}
|
||||
/* no-JS footer fallback (rendered only when JavaScript is off) */
|
||||
.footer .bc-noscript {
|
||||
margin-top: 0.6rem;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
.footer .bc-noscript a {
|
||||
color: var(--brass);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
</style>
|
||||
<script type="application/ld+json">
|
||||
{
|
||||
@@ -501,7 +613,7 @@
|
||||
},
|
||||
"offers": {
|
||||
"@type": "Offer",
|
||||
"price": "465.00",
|
||||
"price": "595.00",
|
||||
"priceCurrency": "USD",
|
||||
"category": "Paid",
|
||||
"url": "https://odoo4projects.com/odoo-erp-automation-bootcamp/"
|
||||
@@ -576,6 +688,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Hero -->
|
||||
<main>
|
||||
<section class="hero bc-hero">
|
||||
<div class="container">
|
||||
<p class="hero-kicker">Q1 2027 · 10 LIVE SESSIONS · OLIVER ARNOLD</p>
|
||||
@@ -592,14 +705,21 @@
|
||||
</div>
|
||||
<div class="hero-actions">
|
||||
<form method="post" action="https://20.odoo4projects.com/shop/post_basket" target="_blank" class="plausible-event-name=Buy">
|
||||
<input type="hidden" name="basket" value='[{"product_id":12,"qty":1}]' />
|
||||
<input type="hidden" name="basket" value='[{"product_id":17,"qty":1,"info":"Q1 2027, English"}]' />
|
||||
<input type="hidden" name="clear" value="1" />
|
||||
<button type="submit" class="btn btn-brass btn-lg">Buy for 465.00 USD</button>
|
||||
<button type="submit" class="btn btn-brass btn-lg">Buy for 595.00 USD</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Live remaining-seat count, immediately below the hero. One seat element only on
|
||||
the page; /static/bootcamp-availability.js fills it from the availability webhook
|
||||
and leaves it empty+hidden if that fetch fails. -->
|
||||
<div class="container">
|
||||
<p class="bc-note bc-seats" data-bc-seats hidden></p>
|
||||
</div>
|
||||
|
||||
<!-- From zero to a running ERP -->
|
||||
<section class="section-light">
|
||||
<div class="container">
|
||||
@@ -615,9 +735,9 @@
|
||||
<p class="bc-subline">Three months. Ten hours of teaching. One system that runs your company.</p>
|
||||
<div class="bc-action">
|
||||
<form method="post" action="https://20.odoo4projects.com/shop/post_basket" target="_blank" class="plausible-event-name=Buy">
|
||||
<input type="hidden" name="basket" value='[{"product_id":12,"qty":1}]' />
|
||||
<input type="hidden" name="basket" value='[{"product_id":17,"qty":1,"info":"Q1 2027, English"}]' />
|
||||
<input type="hidden" name="clear" value="1" />
|
||||
<button type="submit" class="btn btn-brass btn-lg">Buy for 465.00 USD</button>
|
||||
<button type="submit" class="btn btn-brass btn-lg">Buy for 595.00 USD</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
@@ -786,19 +906,38 @@
|
||||
<section class="section-light">
|
||||
<div class="container">
|
||||
<h2 class="section-title">Price and booking</h2>
|
||||
<p class="bc-price">The programme costs USD 465.00 for all ten sessions.</p>
|
||||
<p class="bc-price">The programme costs USD 595.00 for all ten sessions.</p>
|
||||
<p class="bc-note"><strong>Limited to 6 participants</strong> — the cohort is capped, so the instructor can work through your own setup.</p>
|
||||
<p class="bc-note">A seat can be <strong>transferred at any time</strong>, and <strong>cancelled up to two weeks before</strong> the first session on 21 January 2027.</p>
|
||||
<p class="bc-lead">
|
||||
Covers the full programme: ten live sessions, one year of hosted Odoo Community Edition, the
|
||||
alumni community and the expert review.
|
||||
</p>
|
||||
<div class="bc-action">
|
||||
<form method="post" action="https://20.odoo4projects.com/shop/post_basket" target="_blank" class="plausible-event-name=Buy">
|
||||
<input type="hidden" name="basket" value='[{"product_id":12,"qty":1}]' />
|
||||
<input type="hidden" name="basket" value='[{"product_id":17,"qty":1,"info":"Q1 2027, English"}]' />
|
||||
<input type="hidden" name="clear" value="1" />
|
||||
<button type="submit" class="btn btn-brass btn-lg">Buy for 465.00 USD</button>
|
||||
<button type="submit" class="btn btn-brass btn-lg">Buy for 595.00 USD</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Waiting list: for a visitor who cannot attend the January cohort.
|
||||
Secondary to the Buy button above. Same CRM webhook and same payload shape as the
|
||||
site's footer contact form (area:'message'); the copy lives here, in the page's
|
||||
language, and /static/bootcamp-waitlist.js carries no strings. -->
|
||||
<div class="bc-waitlist">
|
||||
<h3 class="bc-wl-title">Another timezone or language?</h3>
|
||||
<p class="bc-wl-lead">The January cohort runs Thursdays 18:00–19:00 Central Time, in English. If you need a different time or another language, join the waiting list and we will write to you when a cohort that fits opens.</p>
|
||||
<form id="bcWaitlistForm" class="bc-wl-form" action="https://n8n.odoo4projects.com/webhook/ODOO4projects/crm" method="post" enctype="text/plain">
|
||||
<label class="bc-sr-only" for="bcWaitlistEmail">Your email address</label>
|
||||
<input type="email" id="bcWaitlistEmail" name="email" placeholder="Your email address" required />
|
||||
<label class="bc-sr-only" for="bcWaitlistComment">Which timezone or language you need</label>
|
||||
<textarea id="bcWaitlistComment" name="comment" rows="2" placeholder="Which timezone or language do you need?" required></textarea>
|
||||
<button type="submit" class="btn bc-wl-btn" data-sending="Adding you…">Join the waiting list</button>
|
||||
</form>
|
||||
<p class="bc-wl-success" id="bcWaitlistSuccess" role="status" style="display:none">You are on the waiting list. We will write to you as soon as a cohort in your timezone or language is announced.</p>
|
||||
<p class="bc-wl-error" id="bcWaitlistError" role="alert" style="display:none">Something went wrong. Please try again.</p>
|
||||
</div>
|
||||
<ul class="bc-related">
|
||||
<li><a href="/support.html#contact">Questions before you book? Talk to us first</a></li>
|
||||
<li><a href="/workshops/">All our live courses, side by side</a></li>
|
||||
@@ -848,6 +987,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<!-- Footer (copied from index.html; icon-font glyphs replaced with inline SVG so the page
|
||||
carries no external icon dependency) -->
|
||||
@@ -855,16 +995,17 @@
|
||||
<div class="container">
|
||||
<div class="fgrid">
|
||||
<div>
|
||||
<h4>Useful Links</h4>
|
||||
<h3>Useful Links</h3>
|
||||
<ul>
|
||||
<li><a href="/odoo-erp-automation-bootcamp/">ODOO Bootcamp</a></li>
|
||||
<li><a href="/odoo-hosting-2027.html">ODOO Hosting in 2027</a></li>
|
||||
<li><a href="/odoo-community-developers/">Odoo Freelancer Directory</a></li>
|
||||
<li><a href="/support.html">Support</a></li>
|
||||
<li><a href="/legal.html">Legal</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<h4>Partners</h4>
|
||||
<h3>Partners</h3>
|
||||
<div class="partner-list">
|
||||
<a href="https://oc-guy.com" target="_blank" rel="noopener" class="partner-item"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#c9a227" stroke-width="2" aria-hidden="true"><path d="M20 7h-9M14 17H5M20 7l-4-4M20 7l-4 4M4 17l4 4M4 17l4-4" /></svg> OC Guy</a>
|
||||
<a href="https://oca-py.org" target="_blank" rel="noopener" class="partner-item"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#c9a227" stroke-width="2" aria-hidden="true"><path d="M20 7h-9M14 17H5M20 7l-4-4M20 7l-4 4M4 17l4 4M4 17l4-4" /></svg> OCA Paraguay</a>
|
||||
@@ -872,17 +1013,17 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="fcol-desc">
|
||||
<h4>About us</h4>
|
||||
<h3>About us</h3>
|
||||
<p>We build smart, scalable business solutions by combining the <strong>stability</strong> of Odoo ERP with the <strong>power</strong> of AI Agents and N8N automations.</p>
|
||||
<p>From streamlined sales to global rollouts, our <strong>lean, eco-friendly</strong> approach helps businesses cut costs, boost efficiency, and grow without limits.</p>
|
||||
</div>
|
||||
<div>
|
||||
<h4>Follow us on</h4>
|
||||
<h3>Follow us on</h3>
|
||||
<div class="social">
|
||||
<a href="https://www.linkedin.com/company/odoo4projects/" target="_blank" aria-label="LinkedIn"><svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M4.98 3.5a2.5 2.5 0 1 1 0 5 2.5 2.5 0 0 1 0-5zM3 9h4v12H3zM9 9h3.8v1.7h.05c.53-.95 1.83-1.95 3.76-1.95 4.02 0 4.76 2.5 4.76 5.75V21h-4v-5.5c0-1.31-.02-3-1.9-3-1.9 0-2.19 1.42-2.19 2.9V21H9z" /></svg></a>
|
||||
<a href="https://www.youtube.com/@ODOO4projects" target="_blank" aria-label="YouTube"><svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M23 12s0-3.2-.41-4.73a2.5 2.5 0 0 0-1.76-1.77C19.3 5.1 12 5.1 12 5.1s-7.3 0-8.83.4A2.5 2.5 0 0 0 1.4 7.27C1 8.8 1 12 1 12s0 3.2.41 4.73a2.5 2.5 0 0 0 1.76 1.77c1.53.4 8.83.4 8.83.4s7.3 0 8.83-.4a2.5 2.5 0 0 0 1.76-1.77C23 15.2 23 12 23 12zM9.75 15.02V8.98L15.5 12z" /></svg></a>
|
||||
</div>
|
||||
<form class="footer-contact-form" id="footerContactForm" action="mailto:hello@odoo4projects.com" method="post" enctype="text/plain">
|
||||
<form class="footer-contact-form" id="footerContactForm" action="https://n8n.odoo4projects.com/webhook/ODOO4projects/crm" method="post" enctype="text/plain" data-ok="Thanks! We'll get back to you shortly." data-err="Something went wrong. Please try again.">
|
||||
<label class="bc-sr-only" for="footerContactEmail">Your email address</label>
|
||||
<input type="email" id="footerContactEmail" name="email" placeholder="Your email address" required />
|
||||
<div class="footer-textarea-wrap">
|
||||
@@ -892,12 +1033,15 @@
|
||||
</div>
|
||||
<div class="footer-form-status" id="footerFormStatus" role="status"></div>
|
||||
</form>
|
||||
<noscript><p class="bc-noscript">JavaScript is off. Email us directly at <a href="mailto:support@odoo4projects.com">support@odoo4projects.com</a>.</p></noscript>
|
||||
</div>
|
||||
</div>
|
||||
<div class="fcopy">Copyright © ODOO4projects LLC</div>
|
||||
</div>
|
||||
</footer>
|
||||
<script defer src="/static/buy.js"></script>
|
||||
<script defer src="/static/lang.js"></script>
|
||||
<script defer src="/static/buy.js?v=31314de600"></script>
|
||||
<script defer src="/static/bootcamp-availability.js?v=94dfd8cd7c"></script>
|
||||
<script defer src="/static/bootcamp-waitlist.js?v=6c2e2fbd33"></script>
|
||||
<script defer src="/static/lang.js?v=3ec49a7e62"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -31,8 +31,7 @@
|
||||
onload="this.media='all'"
|
||||
/>
|
||||
<noscript><link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Marcellus&display=swap" /></noscript>
|
||||
<link rel="stylesheet" href="style.css" media="print" onload="this.media='all'" />
|
||||
<noscript><link rel="stylesheet" href="style.css" /></noscript>
|
||||
<link rel="stylesheet" href="style.css?v=061517baef" />
|
||||
<style>
|
||||
/* ---- Odoo Hosting 2027 landing page (page-scoped; additive) ---- */
|
||||
.cmp-sr-only {
|
||||
@@ -479,7 +478,7 @@
|
||||
{
|
||||
"@type": "Product",
|
||||
"name": "Managed Odoo Community hosting",
|
||||
"description": "Managed Odoo Community hosting: the server, backups, SSL certificate and version upgrades are run for you on one yearly price, with no per-user licence on Odoo Community.",
|
||||
"description": "Managed Odoo Community hosting: the server, backups and SSL certificate are run for you and your Odoo is kept current within its version, on one yearly price with no per-user licence on Odoo Community. Moving to a new major version is a migration and is quoted separately.",
|
||||
"brand": { "@id": "https://odoo4projects.com/#organization" },
|
||||
"offers": [
|
||||
{
|
||||
@@ -560,7 +559,7 @@
|
||||
window.plausible=window.plausible||function(){(plausible.q=plausible.q||[]).push(arguments)},plausible.init=plausible.init||function(i){plausible.o=i||{}};
|
||||
plausible.init()
|
||||
</script>
|
||||
<script defer src="/static/lang.js"></script>
|
||||
<script defer src="/static/lang.js?v=3ec49a7e62"></script>
|
||||
</head>
|
||||
<body>
|
||||
<!-- Page-scoped top bar: the site has no global header, so the page carries its own way home. -->
|
||||
@@ -576,14 +575,15 @@
|
||||
</div>
|
||||
|
||||
<!-- Hero -->
|
||||
<main>
|
||||
<section class="hero cmp-hero">
|
||||
<div class="container">
|
||||
<p class="hero-kicker">Managed Odoo Community Hosting</p>
|
||||
<h1>Odoo Hosting 2027: Managed Odoo Community, <span>Live in About Five Minutes</span></h1>
|
||||
<p class="hero-sub">
|
||||
A managed Odoo Community instance: your stock, orders, invoices and customers in one system. We
|
||||
run the server, the backups, the certificate and the upgrades. You put your own data in and get on
|
||||
with the business.
|
||||
run the server, the backups, the certificate and the updates within your Odoo version. You put
|
||||
your own data in and get on with the business.
|
||||
</p>
|
||||
<div class="hero-stats">
|
||||
<span class="hero-stat"><i aria-hidden="true">✓</i> About 5 minutes to a live instance</span>
|
||||
@@ -606,8 +606,9 @@
|
||||
<p class="cmp-lead">
|
||||
You should not have to become the IT department to find out what is in your stock. A business
|
||||
system is something you subscribe to, not a project you build. The server, the patches, the
|
||||
backups, the certificate, the version upgrade: those are our job, permanently, and not an evening
|
||||
you hand back to the company.
|
||||
backups, the certificate, the updates within your Odoo version: those are our job, permanently,
|
||||
and not an evening you hand back to the company. Moving the database to a new major version is a
|
||||
migration we quote for separately.
|
||||
</p>
|
||||
<p class="cmp-lead">
|
||||
So we put your stock, your orders, your invoices and who owes you money in one place, and we keep
|
||||
@@ -704,7 +705,7 @@
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Version upgrades</th>
|
||||
<td class="is-us">A support ticket. We migrate versions 17, 18 and 19.</td>
|
||||
<td class="is-us">Updates and in-version upgrades are included. A move to a new major version is a migration, quoted separately.</td>
|
||||
<td>You do it, after taking a full backup.</td>
|
||||
<td>Push to the branch; if a module update fails, Odoo reverts the server and rolls the database back.</td>
|
||||
<td>Automatic and mandatory on Odoo's schedule.</td>
|
||||
@@ -985,6 +986,7 @@
|
||||
|
||||
<!-- Footer (copied from index.html; icon-font glyphs replaced with inline SVG so the page
|
||||
carries no external icon dependency) -->
|
||||
</main>
|
||||
<footer class="footer">
|
||||
<div class="container">
|
||||
<div class="fgrid">
|
||||
@@ -993,6 +995,7 @@
|
||||
<ul>
|
||||
<li><a href="/odoo-erp-automation-bootcamp/">ODOO Bootcamp</a></li>
|
||||
<li><a href="odoo-hosting-2027.html">ODOO Hosting in 2027</a></li>
|
||||
<li><a href="/odoo-community-developers/">Odoo Freelancer Directory</a></li>
|
||||
<li><a href="support.html">Support</a></li>
|
||||
<li><a href="legal.html">Legal</a></li>
|
||||
</ul>
|
||||
@@ -1016,7 +1019,7 @@
|
||||
<a href="https://www.linkedin.com/company/odoo4projects/" target="_blank" aria-label="LinkedIn"><svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M4.98 3.5a2.5 2.5 0 1 1 0 5 2.5 2.5 0 0 1 0-5zM3 9h4v12H3zM9 9h3.8v1.7h.05c.53-.95 1.83-1.95 3.76-1.95 4.02 0 4.76 2.5 4.76 5.75V21h-4v-5.5c0-1.31-.02-3-1.9-3-1.9 0-2.19 1.42-2.19 2.9V21H9z" /></svg></a>
|
||||
<a href="https://www.youtube.com/@ODOO4projects" target="_blank" aria-label="YouTube"><svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M23 12s0-3.2-.41-4.73a2.5 2.5 0 0 0-1.76-1.77C19.3 5.1 12 5.1 12 5.1s-7.3 0-8.83.4A2.5 2.5 0 0 0 1.4 7.27C1 8.8 1 12 1 12s0 3.2.41 4.73a2.5 2.5 0 0 0 1.76 1.77c1.53.4 8.83.4 8.83.4s7.3 0 8.83-.4a2.5 2.5 0 0 0 1.76-1.77C23 15.2 23 12 23 12zM9.75 15.02V8.98L15.5 12z" /></svg></a>
|
||||
</div>
|
||||
<form class="footer-contact-form" id="footerContactForm" action="mailto:hello@odoo4projects.com" method="post" enctype="text/plain">
|
||||
<form class="footer-contact-form" id="footerContactForm" action="mailto:support@odoo4projects.com" method="post" enctype="text/plain">
|
||||
<label class="cmp-sr-only" for="footerContactEmail">Your email address</label>
|
||||
<input type="email" id="footerContactEmail" name="email" placeholder="Your email address" required />
|
||||
<div class="footer-textarea-wrap">
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
<meta property="og:image:height" content="630" />
|
||||
<meta
|
||||
property="og:image:alt"
|
||||
content="Black Friday 2026 poster: 'Black Friday — 16–30 November' and 'Save $100 on Odoo hosting', with a secret deal on the Bootcamp."
|
||||
content="Black Friday 2026 poster: 'Black Friday — 2–30 November' and 'Save $100 on Odoo hosting', with a secret deal on the Bootcamp."
|
||||
/>
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<meta name="twitter:image" content="https://odoo4projects.com/images/black-friday/bf2026-hosting-og.png" />
|
||||
@@ -35,8 +35,7 @@
|
||||
onload="this.media='all'"
|
||||
/>
|
||||
<noscript><link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Marcellus&display=swap" /></noscript>
|
||||
<link rel="stylesheet" href="/style.css" media="print" onload="this.media='all'" />
|
||||
<noscript><link rel="stylesheet" href="/style.css" /></noscript>
|
||||
<link rel="stylesheet" href="/style.css?v=061517baef" />
|
||||
<style>
|
||||
/* ---- Odoo Hosting Offers (evergreen hub) page (page-scoped; additive) ---- */
|
||||
.of-sr-only {
|
||||
@@ -384,7 +383,7 @@
|
||||
{
|
||||
"@type": "Product",
|
||||
"name": "On the Rise — managed Odoo Community hosting",
|
||||
"description": "Managed Odoo Community hosting on the On the Rise plan: the server, backups, SSL certificate and version upgrades are run for you on one yearly price, with no per-user licence and no setup fee.",
|
||||
"description": "Managed Odoo Community hosting on the On the Rise plan: the server, backups and SSL certificate are run for you and your Odoo is kept current within its version, on one yearly price with no per-user licence and no setup fee. Moving to a new major version is a migration and is quoted separately.",
|
||||
"brand": { "@id": "https://odoo4projects.com/#organization" },
|
||||
"offers": [
|
||||
{
|
||||
@@ -450,7 +449,7 @@
|
||||
window.plausible=window.plausible||function(){(plausible.q=plausible.q||[]).push(arguments)},plausible.init=plausible.init||function(i){plausible.o=i||{}};
|
||||
plausible.init()
|
||||
</script>
|
||||
<script defer src="/static/lang.js"></script>
|
||||
<script defer src="/static/lang.js?v=3ec49a7e62"></script>
|
||||
</head>
|
||||
<body>
|
||||
<!-- Page-scoped top bar: the site has no global header, so the page carries its own way home. -->
|
||||
@@ -466,6 +465,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Hero -->
|
||||
<main>
|
||||
<section class="hero of-hero">
|
||||
<div class="container">
|
||||
<h1>Odoo hosting offers, and what a year costs</h1>
|
||||
@@ -479,12 +479,14 @@
|
||||
<!-- Date-scoped Black Friday block -->
|
||||
<section class="section-light">
|
||||
<div class="container">
|
||||
<!-- BF-BLOCK-START: live 16–30 November 2026 only. Remove this block after 30 Nov 2026. -->
|
||||
<!-- BF-BLOCK-START: live 2–30 November 2026 only. Remove this block after 30 Nov 2026. -->
|
||||
<div class="bf-hub">
|
||||
<h2>Black Friday and Cyber Monday 2026</h2>
|
||||
<p class="bf-hub-line">This November we take $100 off your first year of hosting. The window is 16–30 November 2026, and it ends on Cyber Monday.</p>
|
||||
<p class="bf-hub-line">This November we take $100 off your first year of hosting. The window is 2–30 November 2026, and it ends on Cyber Monday.</p>
|
||||
<!-- 2026-11-01, dated for removal: the offer and the code are revealed on /odoo-black-friday/ only. Remove this link with the BF block after 30 November 2026. -->
|
||||
<p class="bf-hub-link"><a href="/odoo-black-friday/">See the Black Friday offer</a></p>
|
||||
<!-- Buy = CLICK PROXY (offer-CTA click), NOT a sale. The real purchase event is fired on the
|
||||
checkout host (20.odoo4projects.com) by odoo-dev. See wiki/concepts/conversion-tracking-contract. -->
|
||||
<p class="bf-hub-link"><a class="plausible-event-name=Buy" href="/odoo-black-friday/">See the Black Friday offer</a></p>
|
||||
</div>
|
||||
<!-- BF-BLOCK-END -->
|
||||
</div>
|
||||
@@ -494,7 +496,7 @@
|
||||
<section class="section-dark">
|
||||
<div class="container">
|
||||
<h2 class="section-title">What a hosting offer from us actually is</h2>
|
||||
<p class="of-lead">A hosting offer from us is one flat price for a year. That price runs your whole business system, and we run everything under it: the server, the backups, your certificate and the version upgrades.</p>
|
||||
<p class="of-lead">A hosting offer from us is one flat price for a year. That price runs your whole business system, and we run everything under it: the server, the backups, your certificate, and the updates that keep your Odoo current within its version. Moving to a new major version is a migration, and we quote that separately.</p>
|
||||
<p class="of-lead">What is not on the bill: a charge for each person who logs in, and a fee to set you up. Odoo Community has no per-user licence, so there is nothing per seat to pay, and there is no setup fee.</p>
|
||||
<p class="of-lead">Start with a 30-day free trial and no card. If it fits, the year starts when you pay, not when you sign up.</p>
|
||||
</div>
|
||||
@@ -566,6 +568,7 @@
|
||||
|
||||
<!-- Footer (copied from index.html; icon-font glyphs replaced with inline SVG so the page
|
||||
carries no external icon dependency) -->
|
||||
</main>
|
||||
<footer class="footer">
|
||||
<div class="container">
|
||||
<div class="fgrid">
|
||||
@@ -574,6 +577,7 @@
|
||||
<ul>
|
||||
<li><a href="/odoo-erp-automation-bootcamp/">ODOO Bootcamp</a></li>
|
||||
<li><a href="/odoo-hosting-2027.html">ODOO Hosting in 2027</a></li>
|
||||
<li><a href="/odoo-community-developers/">Odoo Freelancer Directory</a></li>
|
||||
<li><a href="/support.html">Support</a></li>
|
||||
<li><a href="/legal.html">Legal</a></li>
|
||||
</ul>
|
||||
@@ -597,7 +601,7 @@
|
||||
<a href="https://www.linkedin.com/company/odoo4projects/" target="_blank" aria-label="LinkedIn"><svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M4.98 3.5a2.5 2.5 0 1 1 0 5 2.5 2.5 0 0 1 0-5zM3 9h4v12H3zM9 9h3.8v1.7h.05c.53-.95 1.83-1.95 3.76-1.95 4.02 0 4.76 2.5 4.76 5.75V21h-4v-5.5c0-1.31-.02-3-1.9-3-1.9 0-2.19 1.42-2.19 2.9V21H9z" /></svg></a>
|
||||
<a href="https://www.youtube.com/@ODOO4projects" target="_blank" aria-label="YouTube"><svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M23 12s0-3.2-.41-4.73a2.5 2.5 0 0 0-1.76-1.77C19.3 5.1 12 5.1 12 5.1s-7.3 0-8.83.4A2.5 2.5 0 0 0 1.4 7.27C1 8.8 1 12 1 12s0 3.2.41 4.73a2.5 2.5 0 0 0 1.76 1.77c1.53.4 8.83.4 8.83.4s7.3 0 8.83-.4a2.5 2.5 0 0 0 1.76-1.77C23 15.2 23 12 23 12zM9.75 15.02V8.98L15.5 12z" /></svg></a>
|
||||
</div>
|
||||
<form class="footer-contact-form" id="footerContactForm" action="mailto:hello@odoo4projects.com" method="post" enctype="text/plain">
|
||||
<form class="footer-contact-form" id="footerContactForm" action="mailto:support@odoo4projects.com" method="post" enctype="text/plain">
|
||||
<label class="of-sr-only" for="footerContactEmail">Your email address</label>
|
||||
<input type="email" id="footerContactEmail" name="email" placeholder="Your email address" required />
|
||||
<div class="footer-textarea-wrap">
|
||||
|
||||
@@ -27,8 +27,7 @@
|
||||
onload="this.media='all'"
|
||||
/>
|
||||
<noscript><link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Marcellus&display=swap" /></noscript>
|
||||
<link rel="stylesheet" href="/style.css" media="print" onload="this.media='all'" />
|
||||
<noscript><link rel="stylesheet" href="/style.css" /></noscript>
|
||||
<link rel="stylesheet" href="/style.css?v=061517baef" />
|
||||
<style>
|
||||
/* ---- ODOO x N8N Masterclass landing page (page-scoped; additive).
|
||||
The block below is the bootcamp page's page-scoped CSS, reused so the two
|
||||
@@ -558,6 +557,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Hero -->
|
||||
<main>
|
||||
<section class="hero bc-hero">
|
||||
<div class="container">
|
||||
<p class="hero-kicker">2 HOURS · LIVE · OLIVER ARNOLD · ENGLISH</p>
|
||||
@@ -783,6 +783,7 @@
|
||||
|
||||
<!-- Footer (copied from index.html; icon-font glyphs replaced with inline SVG so the page
|
||||
carries no external icon dependency) -->
|
||||
</main>
|
||||
<footer class="footer">
|
||||
<div class="container">
|
||||
<div class="fgrid">
|
||||
@@ -791,6 +792,7 @@
|
||||
<ul>
|
||||
<li><a href="/odoo-erp-automation-bootcamp/">ODOO Bootcamp</a></li>
|
||||
<li><a href="/odoo-hosting-2027.html">ODOO Hosting in 2027</a></li>
|
||||
<li><a href="/odoo-community-developers/">Odoo Freelancer Directory</a></li>
|
||||
<li><a href="/support.html">Support</a></li>
|
||||
<li><a href="/legal.html">Legal</a></li>
|
||||
</ul>
|
||||
@@ -814,7 +816,7 @@
|
||||
<a href="https://www.linkedin.com/company/odoo4projects/" target="_blank" aria-label="LinkedIn"><svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M4.98 3.5a2.5 2.5 0 1 1 0 5 2.5 2.5 0 0 1 0-5zM3 9h4v12H3zM9 9h3.8v1.7h.05c.53-.95 1.83-1.95 3.76-1.95 4.02 0 4.76 2.5 4.76 5.75V21h-4v-5.5c0-1.31-.02-3-1.9-3-1.9 0-2.19 1.42-2.19 2.9V21H9z" /></svg></a>
|
||||
<a href="https://www.youtube.com/@ODOO4projects" target="_blank" aria-label="YouTube"><svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M23 12s0-3.2-.41-4.73a2.5 2.5 0 0 0-1.76-1.77C19.3 5.1 12 5.1 12 5.1s-7.3 0-8.83.4A2.5 2.5 0 0 0 1.4 7.27C1 8.8 1 12 1 12s0 3.2.41 4.73a2.5 2.5 0 0 0 1.76 1.77c1.53.4 8.83.4 8.83.4s7.3 0 8.83-.4a2.5 2.5 0 0 0 1.76-1.77C23 15.2 23 12 23 12zM9.75 15.02V8.98L15.5 12z" /></svg></a>
|
||||
</div>
|
||||
<form class="footer-contact-form" id="footerContactForm" action="mailto:hello@odoo4projects.com" method="post" enctype="text/plain">
|
||||
<form class="footer-contact-form" id="footerContactForm" action="mailto:support@odoo4projects.com" method="post" enctype="text/plain">
|
||||
<label class="bc-sr-only" for="footerContactEmail">Your email address</label>
|
||||
<input type="email" id="footerContactEmail" name="email" placeholder="Your email address" required />
|
||||
<div class="footer-textarea-wrap">
|
||||
@@ -829,7 +831,7 @@
|
||||
<div class="fcopy">Copyright © ODOO4projects LLC</div>
|
||||
</div>
|
||||
</footer>
|
||||
<script defer src="/static/buy.js"></script>
|
||||
<script defer src="/static/lang.js"></script>
|
||||
<script defer src="/static/buy.js?v=31314de600"></script>
|
||||
<script defer src="/static/lang.js?v=3ec49a7e62"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -29,8 +29,7 @@
|
||||
onload="this.media='all'"
|
||||
/>
|
||||
<noscript><link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Marcellus&display=swap" /></noscript>
|
||||
<link rel="stylesheet" href="/style.css" media="print" onload="this.media='all'" />
|
||||
<noscript><link rel="stylesheet" href="/style.css" /></noscript>
|
||||
<link rel="stylesheet" href="/style.css?v=061517baef" />
|
||||
<style>
|
||||
/* ---- "Odoo staging environment" guide page (page-scoped; additive).
|
||||
Reuses the top bar, band rhythm, hero and FAQ treatment already shipped on
|
||||
@@ -475,6 +474,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Hero: the reader's question, answered before the first H2 -->
|
||||
<main>
|
||||
<section class="hero oc-hero">
|
||||
<div class="hero-bg">
|
||||
<img
|
||||
@@ -774,6 +774,7 @@
|
||||
|
||||
<!-- Footer (copied from what-can-i-use-odoo-for/; icon-font glyphs replaced with inline SVG so the page
|
||||
carries no external icon dependency) -->
|
||||
</main>
|
||||
<footer class="footer">
|
||||
<div class="container">
|
||||
<div class="fgrid">
|
||||
@@ -782,6 +783,7 @@
|
||||
<ul>
|
||||
<li><a href="/odoo-erp-automation-bootcamp/">ODOO Bootcamp</a></li>
|
||||
<li><a href="/odoo-hosting-2027.html">ODOO Hosting in 2027</a></li>
|
||||
<li><a href="/odoo-community-developers/">Odoo Freelancer Directory</a></li>
|
||||
<li><a href="/support.html">Support</a></li>
|
||||
<li><a href="/legal.html">Legal</a></li>
|
||||
</ul>
|
||||
@@ -805,7 +807,7 @@
|
||||
<a href="https://www.linkedin.com/company/odoo4projects/" target="_blank" aria-label="LinkedIn"><svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M4.98 3.5a2.5 2.5 0 1 1 0 5 2.5 2.5 0 0 1 0-5zM3 9h4v12H3zM9 9h3.8v1.7h.05c.53-.95 1.83-1.95 3.76-1.95 4.02 0 4.76 2.5 4.76 5.75V21h-4v-5.5c0-1.31-.02-3-1.9-3-1.9 0-2.19 1.42-2.19 2.9V21H9z" /></svg></a>
|
||||
<a href="https://www.youtube.com/@ODOO4projects" target="_blank" aria-label="YouTube"><svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M23 12s0-3.2-.41-4.73a2.5 2.5 0 0 0-1.76-1.77C19.3 5.1 12 5.1 12 5.1s-7.3 0-8.83.4A2.5 2.5 0 0 0 1.4 7.27C1 8.8 1 12 1 12s0 3.2.41 4.73a2.5 2.5 0 0 0 1.76 1.77c1.53.4 8.83.4 8.83.4s7.3 0 8.83-.4a2.5 2.5 0 0 0 1.76-1.77C23 15.2 23 12 23 12zM9.75 15.02V8.98L15.5 12z" /></svg></a>
|
||||
</div>
|
||||
<form class="footer-contact-form" id="footerContactForm" action="mailto:hello@odoo4projects.com" method="post" enctype="text/plain">
|
||||
<form class="footer-contact-form" id="footerContactForm" action="mailto:support@odoo4projects.com" method="post" enctype="text/plain">
|
||||
<label class="oc-sr-only" for="footerContactEmail">Your email address</label>
|
||||
<input type="email" id="footerContactEmail" name="email" placeholder="Your email address" required />
|
||||
<div class="footer-textarea-wrap">
|
||||
@@ -820,6 +822,6 @@
|
||||
<div class="fcopy">Copyright © ODOO4projects LLC</div>
|
||||
</div>
|
||||
</footer>
|
||||
<script defer src="/static/lang.js"></script>
|
||||
<script defer src="/static/lang.js?v=3ec49a7e62"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -210,6 +210,7 @@
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<div class="page-subtitle">
|
||||
You <b>immediately</b> unlock all new features and higher limits.<br />
|
||||
Any extra time is <b>added</b> to your existing subscription.
|
||||
@@ -492,5 +493,6 @@
|
||||
|
||||
loadContract();
|
||||
</script>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,705 @@
|
||||
<!doctype html>
|
||||
<html lang="en-US">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Odoo Community as a first-class citizen | ODOO4projects</title>
|
||||
<meta
|
||||
name="description"
|
||||
content="Managed Odoo Community hosting: live in about five minutes, six regions, rotating backups, free SSL, Git module deploys and an AI support bot."
|
||||
/>
|
||||
<link rel="canonical" href="https://odoo4projects.com/odooityourself.html" />
|
||||
<meta property="og:title" content="Odoo Community as a first-class citizen" />
|
||||
<meta
|
||||
property="og:description"
|
||||
content="Managed Odoo Community hosting: live in about five minutes, six regions, rotating backups, free SSL, Git module deploys and an AI support bot."
|
||||
/>
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:url" content="https://odoo4projects.com/odooityourself.html" />
|
||||
<meta name="twitter:card" content="summary" />
|
||||
<link rel="icon" href="/images/favicon.ico" />
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Marcellus&display=swap"
|
||||
media="print"
|
||||
onload="this.media='all'"
|
||||
/>
|
||||
<noscript><link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Marcellus&display=swap" /></noscript>
|
||||
<link rel="stylesheet" href="/style.css?v=061517baef" media="print" onload="this.media='all'" />
|
||||
<noscript><link rel="stylesheet" href="/style.css?v=061517baef" /></noscript>
|
||||
<style>
|
||||
/* ---- Partner briefing for Andrew (page-scoped; additive) ---- */
|
||||
.andrew-sr-only {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
padding: 0;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
white-space: nowrap;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
/* Top bar (page-scoped: the site has no global header) */
|
||||
.bc-topbar {
|
||||
background: var(--midnight-2);
|
||||
border-bottom: 1px solid var(--line-brass);
|
||||
padding: 0.85rem 0;
|
||||
}
|
||||
.bc-topbar .container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.bc-brand {
|
||||
font-family: Marcellus, Georgia, serif;
|
||||
font-size: 1.15rem;
|
||||
letter-spacing: 0.04em;
|
||||
color: #fff;
|
||||
}
|
||||
.bc-brand span {
|
||||
color: var(--brass);
|
||||
}
|
||||
.bc-topbar-links {
|
||||
display: flex;
|
||||
gap: 1.25rem;
|
||||
font-size: 0.82rem;
|
||||
margin-left: auto;
|
||||
}
|
||||
.bc-topbar-links a {
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
}
|
||||
.bc-topbar-links a:hover {
|
||||
color: var(--brass);
|
||||
}
|
||||
.bc-topbar .lang-switch-slot {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-left: 1rem;
|
||||
border-left: 1px solid rgba(201, 162, 39, 0.45);
|
||||
}
|
||||
.bc-topbar .lang-switch {
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
}
|
||||
.bc-topbar .lang-switch a:hover,
|
||||
.bc-topbar .lang-switch a:focus-visible {
|
||||
color: var(--brass-bright);
|
||||
}
|
||||
.bc-topbar .lang-switch a[aria-current="true"] {
|
||||
color: var(--brass);
|
||||
text-decoration: underline;
|
||||
text-underline-offset: 3px;
|
||||
}
|
||||
|
||||
/* Hero — art-director hero-spec.css, applied as given */
|
||||
.andrew-hero {
|
||||
position: relative;
|
||||
min-height: 620px;
|
||||
background: var(--midnight);
|
||||
overflow: hidden;
|
||||
}
|
||||
.andrew-hero::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 0;
|
||||
background-image: url('/images/hero/bootcamp-hero-desktop-1920.webp');
|
||||
background-size: cover;
|
||||
background-repeat: no-repeat;
|
||||
background-position: 50% 40%;
|
||||
}
|
||||
.andrew-hero::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 0;
|
||||
background: linear-gradient(90deg,
|
||||
rgba(13, 27, 46, .75) 0%,
|
||||
rgba(13, 27, 46, .75) 60%,
|
||||
rgba(13, 27, 46, .55) 72%,
|
||||
rgba(13, 27, 46, .55) 100%);
|
||||
}
|
||||
.andrew-hero .container {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
width: 86%;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding-top: 104px;
|
||||
padding-bottom: 3rem;
|
||||
}
|
||||
.andrew-hero .hc-h1 {
|
||||
font-family: Marcellus, Georgia, serif;
|
||||
font-weight: 400;
|
||||
font-size: 3.6rem;
|
||||
line-height: 1.12;
|
||||
letter-spacing: 0.02em;
|
||||
color: var(--cream);
|
||||
max-width: 720px;
|
||||
margin: 0 0 0.75rem;
|
||||
}
|
||||
.andrew-kicker {
|
||||
font-family: Inter, sans-serif;
|
||||
font-size: 0.72rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.15em;
|
||||
text-transform: uppercase;
|
||||
color: var(--brass);
|
||||
margin-bottom: 0.85rem;
|
||||
}
|
||||
.andrew-sub {
|
||||
font-family: Marcellus, Georgia, serif;
|
||||
font-size: 1.2rem;
|
||||
letter-spacing: 0.02em;
|
||||
color: var(--brass-bright);
|
||||
max-width: 720px;
|
||||
margin: 0 0 0.85rem;
|
||||
}
|
||||
.andrew-lead {
|
||||
font-size: 1rem;
|
||||
line-height: 1.7;
|
||||
color: rgba(255, 255, 255, 0.82);
|
||||
max-width: 640px;
|
||||
margin: 0 0 1.5rem;
|
||||
}
|
||||
@media (max-width: 899px) {
|
||||
.andrew-hero {
|
||||
min-height: 760px;
|
||||
}
|
||||
.andrew-hero::before {
|
||||
background-image: url('/images/hero/bootcamp-hero-mobile-900.webp');
|
||||
background-position: 50% 50%;
|
||||
}
|
||||
.andrew-hero::after {
|
||||
background: linear-gradient(180deg,
|
||||
rgba(13, 27, 46, .75) 0%,
|
||||
rgba(13, 27, 46, .75) 62%,
|
||||
rgba(13, 27, 46, .55) 100%);
|
||||
}
|
||||
.andrew-hero .container {
|
||||
padding-top: 120px;
|
||||
}
|
||||
.andrew-hero .hc-h1 {
|
||||
font-size: 2rem;
|
||||
max-width: 100%;
|
||||
}
|
||||
.andrew-sub {
|
||||
font-size: 1.05rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Body copy shared by the sections */
|
||||
.andrew-body {
|
||||
font-size: 1rem;
|
||||
line-height: 1.75;
|
||||
color: rgba(23, 28, 38, 0.78);
|
||||
max-width: 820px;
|
||||
margin: 0 auto 1.25rem;
|
||||
}
|
||||
.andrew-body.on-dark {
|
||||
color: rgba(255, 255, 255, 0.72);
|
||||
}
|
||||
.andrew-body a {
|
||||
color: var(--brass-deep);
|
||||
text-decoration: underline;
|
||||
text-underline-offset: 2px;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
.andrew-body.on-dark a {
|
||||
color: var(--brass-bright);
|
||||
}
|
||||
.andrew-h3 {
|
||||
font-family: Marcellus, Georgia, serif;
|
||||
font-size: 1.15rem;
|
||||
color: var(--midnight);
|
||||
max-width: 820px;
|
||||
margin: 2rem auto 0.5rem;
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
.andrew-regions {
|
||||
font-family: Marcellus, Georgia, serif;
|
||||
font-size: 1.05rem;
|
||||
letter-spacing: 0.03em;
|
||||
color: var(--brass-deep);
|
||||
max-width: 820px;
|
||||
margin: 0 auto 1.25rem;
|
||||
}
|
||||
.bc-sr-only {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
padding: 0;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
white-space: nowrap;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
/* AI section — copy beside the graphic */
|
||||
.ai-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1.15fr;
|
||||
gap: 2.5rem;
|
||||
align-items: center;
|
||||
max-width: 1100px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.ai-copy p {
|
||||
font-size: 0.95rem;
|
||||
line-height: 1.75;
|
||||
color: rgba(255, 255, 255, 0.78);
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.ai-copy .ai-actions-list {
|
||||
margin: 0 0 1rem;
|
||||
padding-left: 1.2rem;
|
||||
color: rgba(255, 255, 255, 0.85);
|
||||
font-size: 0.95rem;
|
||||
line-height: 1.7;
|
||||
}
|
||||
.ai-copy .ai-actions-list li {
|
||||
margin-bottom: 0.2rem;
|
||||
}
|
||||
.ai-figure {
|
||||
margin: 0;
|
||||
}
|
||||
.ai-figure img {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
border-radius: var(--radius-lg);
|
||||
}
|
||||
.ai-figure .ai-desktop {
|
||||
display: block;
|
||||
}
|
||||
.ai-figure figcaption {
|
||||
margin-top: 0.75rem;
|
||||
font-size: 0.82rem;
|
||||
line-height: 1.6;
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
text-align: center;
|
||||
}
|
||||
.ai-figure .ai-mobile {
|
||||
display: none;
|
||||
}
|
||||
@media (max-width: 899px) {
|
||||
.ai-grid {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 2rem;
|
||||
}
|
||||
.ai-figure .ai-desktop {
|
||||
display: none;
|
||||
}
|
||||
.ai-figure .ai-mobile {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
/* Management console */
|
||||
.console-shot {
|
||||
margin: 0 auto 2rem;
|
||||
max-width: 862px;
|
||||
}
|
||||
.console-shot img {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
border-radius: var(--radius-lg);
|
||||
border: 1px solid var(--line);
|
||||
box-shadow: var(--shadow-hover);
|
||||
}
|
||||
.console-shot figcaption {
|
||||
margin-top: 0.75rem;
|
||||
font-size: 0.85rem;
|
||||
line-height: 1.65;
|
||||
color: var(--ink-soft);
|
||||
text-align: center;
|
||||
}
|
||||
.console-list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0 auto 1.75rem;
|
||||
max-width: 720px;
|
||||
display: grid;
|
||||
gap: 0.6rem;
|
||||
}
|
||||
.console-list li {
|
||||
position: relative;
|
||||
padding-left: 1.5rem;
|
||||
font-size: 0.92rem;
|
||||
line-height: 1.6;
|
||||
color: var(--ink);
|
||||
}
|
||||
.console-list li::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 0.25rem;
|
||||
top: 0.55rem;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
background: var(--brass);
|
||||
}
|
||||
.pro-tip {
|
||||
max-width: 820px;
|
||||
margin: 0 auto;
|
||||
background: var(--ivory);
|
||||
border: 1px solid var(--line-brass);
|
||||
border-radius: var(--radius);
|
||||
padding: 1rem 1.25rem;
|
||||
font-size: 0.95rem;
|
||||
line-height: 1.7;
|
||||
color: var(--ink);
|
||||
}
|
||||
.pro-tip strong {
|
||||
color: var(--brass-deep);
|
||||
text-transform: uppercase;
|
||||
font-size: 0.78rem;
|
||||
letter-spacing: 0.08em;
|
||||
}
|
||||
|
||||
/* Pricing */
|
||||
.price-list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0 auto 1.5rem;
|
||||
max-width: 480px;
|
||||
display: grid;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
.price-list li {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: baseline;
|
||||
gap: 1rem;
|
||||
padding: 0.7rem 1.1rem;
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
border: 1px solid var(--line-brass);
|
||||
border-radius: var(--radius);
|
||||
}
|
||||
.price-list .plan {
|
||||
font-family: Marcellus, Georgia, serif;
|
||||
font-size: 1.05rem;
|
||||
color: #fff;
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
.price-list .amount {
|
||||
font-size: 0.92rem;
|
||||
color: var(--brass-bright);
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
/* Demo checklist */
|
||||
.checklist {
|
||||
counter-reset: andrew-demo;
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0 auto;
|
||||
max-width: 820px;
|
||||
display: grid;
|
||||
gap: 0.7rem;
|
||||
}
|
||||
.checklist li {
|
||||
counter-increment: andrew-demo;
|
||||
position: relative;
|
||||
padding-left: 2.2rem;
|
||||
font-size: 0.92rem;
|
||||
line-height: 1.65;
|
||||
color: var(--ink);
|
||||
}
|
||||
.checklist li::before {
|
||||
content: counter(andrew-demo);
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-family: Marcellus, Georgia, serif;
|
||||
font-size: 0.85rem;
|
||||
color: var(--midnight);
|
||||
background: var(--brass);
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
/* Focus states */
|
||||
.btn:focus-visible,
|
||||
.bc-topbar-links a:focus-visible,
|
||||
.andrew-body a:focus-visible {
|
||||
outline: 3px solid var(--brass-bright);
|
||||
outline-offset: 2px;
|
||||
border-radius: var(--radius);
|
||||
}
|
||||
</style>
|
||||
<script type="application/ld+json">
|
||||
{
|
||||
"@context": "https://schema.org",
|
||||
"@type": "WebPage",
|
||||
"name": "Odoo Community as a first-class citizen",
|
||||
"description": "Managed Odoo Community hosting: live in about five minutes, six regions, rotating backups, free SSL, Git module deploys and an AI support bot.",
|
||||
"url": "https://odoo4projects.com/odooityourself.html",
|
||||
"inLanguage": "en",
|
||||
"publisher": {
|
||||
"@type": "Organization",
|
||||
"name": "ODOO4projects",
|
||||
"url": "https://odoo4projects.com/"
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<!-- Privacy-friendly analytics by Plausible -->
|
||||
<script async src="https://plausible.office.odoo4projects.com/js/pa-NhA7Rf-79VopXFQXhSS4e.js"></script>
|
||||
<script>
|
||||
window.plausible=window.plausible||function(){(plausible.q=plausible.q||[]).push(arguments)},plausible.init=plausible.init||function(i){plausible.o=i||{}};
|
||||
plausible.init()
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<!-- Page-scoped top bar: the site has no global header, so the page carries its own way home. -->
|
||||
<div class="bc-topbar">
|
||||
<div class="container">
|
||||
<a class="bc-brand" href="/">ODOO4<span>projects</span></a>
|
||||
<nav class="bc-topbar-links" aria-label="Page links">
|
||||
<a href="/index.html#pricing">Plans & pricing</a>
|
||||
<a href="/support.html">Support</a>
|
||||
</nav>
|
||||
<div data-lang-switcher class="lang-switch-slot"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 1. Hero — the vision -->
|
||||
<section class="andrew-hero">
|
||||
<div class="container">
|
||||
<p class="andrew-kicker">A BRIEFING FOR ANDREW · ODOO COMMUNITY HOSTING</p>
|
||||
<h1 class="hc-h1">Odoo Community as a first-class citizen</h1>
|
||||
<p class="andrew-sub">Hosted, managed and treated as a real product, with the ease of other solutions baked in.</p>
|
||||
<p class="andrew-lead">
|
||||
Community is the edition you can own, extend and verify. It should not cost you a weekend of server work
|
||||
to keep it alive. We run it as managed hosting: provisioned in about five minutes, in six regions, with
|
||||
backups, a certificate and updates already handled. You get the edition that rewards an implementer,
|
||||
without the infrastructure chore that usually comes with it.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 2. Features — a 2x3 grid of six cards -->
|
||||
<section class="section-light">
|
||||
<div class="container">
|
||||
<h2 class="section-title">What you get</h2>
|
||||
<div class="features-grid">
|
||||
<div class="feature-card">
|
||||
<h3>Live in about five minutes</h3>
|
||||
<p>Order an instance and use it the same day. No server to order, no stack to install.</p>
|
||||
</div>
|
||||
<div class="feature-card">
|
||||
<h3>Six regions</h3>
|
||||
<p>Amsterdam, Boston, Manchester, Mumbai, São Paulo, Sydney. Put the instance near the team using it.</p>
|
||||
</div>
|
||||
<div class="feature-card">
|
||||
<h3>Backups running, SSL issued</h3>
|
||||
<p>Automatic rotating backup slots and a free SSL certificate on every domain. On Side Hustle one slot; four on On the Rise and Powerhouse.</p>
|
||||
</div>
|
||||
<div class="feature-card">
|
||||
<h3>Modules by Git, not by FTP</h3>
|
||||
<p>Push custom modules with Git, or install them in one click with Quick Git. There is no FTP.</p>
|
||||
</div>
|
||||
<div class="feature-card">
|
||||
<h3>Management inside Odoo</h3>
|
||||
<p>Applications, users and modules from one panel, in the same place you do the work.</p>
|
||||
</div>
|
||||
<div class="feature-card">
|
||||
<h3>A support bot, and humans behind it</h3>
|
||||
<p>The AI bot handles the instant fixes; our team handles the rest. Your UUID is the key.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 3. AI section — the graphic beside the copy -->
|
||||
<section class="section-dark">
|
||||
<div class="container">
|
||||
<h2 class="section-title">An agent that can actually touch the server</h2>
|
||||
<div class="ai-grid">
|
||||
<div class="ai-copy">
|
||||
<p>
|
||||
Every instance ships with an MCP server preinstalled. It is the bridge between your agent and the
|
||||
machine it runs on, and it is not read-only. Through it, an agent can:
|
||||
</p>
|
||||
<ul class="ai-actions-list">
|
||||
<li>restart the server;</li>
|
||||
<li>create a backup, or reinstall one;</li>
|
||||
<li>read the logfiles;</li>
|
||||
<li>install modules.</li>
|
||||
</ul>
|
||||
<p>
|
||||
That turns a support question into a one-line action. Ask for the log tail instead of describing the
|
||||
symptom; ask for a restart instead of waiting for one. The same server also answers in plain language:
|
||||
the support agent in your dashboard takes the question you actually typed, and the human team is behind
|
||||
it when the answer needs a person.
|
||||
</p>
|
||||
</div>
|
||||
<figure class="ai-figure">
|
||||
<img
|
||||
class="ai-desktop"
|
||||
src="/images/andrew/ai-agent-on-server.svg"
|
||||
width="1200"
|
||||
height="800"
|
||||
alt="An agent acting on your Odoo server: restart the server, create a backup, read the logfiles, install a module."
|
||||
/>
|
||||
<img
|
||||
class="ai-mobile"
|
||||
src="/images/andrew/ai-agent-on-server-mobile.svg"
|
||||
width="720"
|
||||
height="1200"
|
||||
alt="An agent acting on your Odoo server: restart the server, create a backup, read the logfiles, install a module."
|
||||
/>
|
||||
<figcaption>The MCP server on every instance: restart, backup, restore, logfiles, modules.</figcaption>
|
||||
</figure>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 4. Management console — one panel for the whole instance -->
|
||||
<section class="section-light">
|
||||
<div class="container">
|
||||
<h2 class="section-title">The console: your Odoo, managed from inside Odoo</h2>
|
||||
<p class="andrew-body">
|
||||
The management console is the central place you run the instance from — no separate admin portal, no
|
||||
SSH habit required. It sits with your Odoo and it is where the operational jobs live.
|
||||
</p>
|
||||
<figure class="console-shot">
|
||||
<img
|
||||
src="https://image.office.odoo4projects.com/img/20_4server.png"
|
||||
width="862"
|
||||
height="627"
|
||||
loading="lazy"
|
||||
alt="The ODOO4projects management console: applications, users, modules, contacts, domain routing and backups in one panel."
|
||||
/>
|
||||
<figcaption>
|
||||
The management console: applications, users, modules, contacts, domain routing and backups, in one panel
|
||||
inside your Odoo.
|
||||
</figcaption>
|
||||
</figure>
|
||||
<ul class="console-list">
|
||||
<li><strong>Contacts</strong> — manage your contacts from your Odoo.</li>
|
||||
<li><strong>Domain routing</strong> — change the domain routing from within your Odoo.</li>
|
||||
<li><strong>Backups and restore</strong> — take a backup, put one back.</li>
|
||||
<li><strong>Git server</strong> — the onboard Git server you install modules from.</li>
|
||||
<li><strong>MCP server</strong> — the agent actions above: restart, backups, logfiles, modules.</li>
|
||||
<li><strong>Support agent</strong> — the support agent in your dashboard.</li>
|
||||
</ul>
|
||||
<p class="pro-tip">
|
||||
<strong>Pro tip:</strong> when your server does not come back because you checked in a bad module, recover it
|
||||
24/7 with the support bot on our homepage.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 5. Pricing -->
|
||||
<section class="section-dark" id="pricing">
|
||||
<div class="container">
|
||||
<h2 class="section-title">Pricing</h2>
|
||||
<p class="andrew-body on-dark">Hosting is billed annually, at list price. No setup fee.</p>
|
||||
<ul class="price-list">
|
||||
<li><span class="plan">Side Hustle</span> <span class="amount">$214 per year</span></li>
|
||||
<li><span class="plan">On the Rise</span> <span class="amount">$395 per year</span></li>
|
||||
<li><span class="plan">Powerhouse</span> <span class="amount">$595 per year</span></li>
|
||||
<li><span class="plan">Empire</span> <span class="amount">quoted for your setup</span></li>
|
||||
</ul>
|
||||
<p class="andrew-body on-dark">
|
||||
Plan details and current pricing: <a href="https://odoo4projects.com">https://odoo4projects.com</a><br />
|
||||
Empire is quoted per setup — request one at
|
||||
<a href="https://odoo4projects.com/support.html#contact">https://odoo4projects.com/support.html#contact</a>
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 6. Some other information -->
|
||||
<section class="section-light">
|
||||
<div class="container">
|
||||
<h2 class="section-title">Some other information</h2>
|
||||
<h3 class="andrew-h3">What we are</h3>
|
||||
<p class="andrew-body">
|
||||
ODOO4projects is managed Odoo Community hosting. We run the servers, the backups, the certificates and the
|
||||
updates. You run your business on the instance, and you can still look under the hood whenever you want to
|
||||
— it is Community, and it is yours to extend.
|
||||
</p>
|
||||
<h3 class="andrew-h3">Where your instance lives</h3>
|
||||
<p class="andrew-regions">Amsterdam · Boston · Manchester · Mumbai · São Paulo · Sydney</p>
|
||||
<h3 class="andrew-h3">Free trial</h3>
|
||||
<p class="andrew-body">
|
||||
Every plan starts with a 30-day free trial, no card required — a trial runs without backups and without
|
||||
a custom domain.
|
||||
</p>
|
||||
<h3 class="andrew-h3">Demo checklist — what to show on camera, in order</h3>
|
||||
<ol class="checklist">
|
||||
<li>Provision a new instance and time it. About five minutes, provisioning to login.</li>
|
||||
<li>Open the management console and walk the panel. Applications, users, modules, contacts, routing, backups.</li>
|
||||
<li>Change a domain route from inside Odoo, then show the free SSL certificate on the new domain.</li>
|
||||
<li>Push a module with Git, then install one in a click with Quick Git. Point out there is no FTP.</li>
|
||||
<li>Ask the agent, over MCP, to read the logfile tail. Then ask it to restart the server.</li>
|
||||
<li>Take a backup, then reinstall it. Show the rotating slots.</li>
|
||||
<li>Break it on purpose: install a bad module, let the server stay down, then recover it with the support bot on the homepage. It is 24/7.</li>
|
||||
</ol>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Footer (copied from index.html; icon-font glyphs replaced with inline SVG so the page
|
||||
carries no external icon dependency) -->
|
||||
<footer class="footer">
|
||||
<div class="container">
|
||||
<div class="fgrid">
|
||||
<div>
|
||||
<h4>Useful Links</h4>
|
||||
<ul>
|
||||
<li><a href="/odoo-erp-automation-bootcamp/">ODOO Bootcamp</a></li>
|
||||
<li><a href="/odoo-hosting-2027.html">ODOO Hosting in 2027</a></li>
|
||||
<li><a href="/odoo-community-developers/">Odoo Freelancer Directory</a></li>
|
||||
<li><a href="/support.html">Support</a></li>
|
||||
<li><a href="/legal.html">Legal</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<h4>Partners</h4>
|
||||
<div class="partner-list">
|
||||
<a href="https://oc-guy.com" target="_blank" rel="noopener" class="partner-item"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#c9a227" stroke-width="2" aria-hidden="true"><path d="M20 7h-9M14 17H5M20 7l-4-4M20 7l-4 4M4 17l4 4M4 17l4-4" /></svg> OC Guy</a>
|
||||
<a href="https://oca-py.org" target="_blank" rel="noopener" class="partner-item"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#c9a227" stroke-width="2" aria-hidden="true"><path d="M20 7h-9M14 17H5M20 7l-4-4M20 7l-4 4M4 17l4 4M4 17l4-4" /></svg> OCA Paraguay</a>
|
||||
<a href="https://OD8N.com" target="_blank" rel="noopener" class="partner-item"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#c9a227" stroke-width="2" aria-hidden="true"><path d="M20 7h-9M14 17H5M20 7l-4-4M20 7l-4 4M4 17l4 4M4 17l4-4" /></svg> OD8N</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="fcol-desc">
|
||||
<h4>About us</h4>
|
||||
<p>We build smart, scalable business solutions by combining the <strong>stability</strong> of Odoo ERP with the <strong>power</strong> of AI Agents and N8N automations.</p>
|
||||
<p>From streamlined sales to global rollouts, our <strong>lean, eco-friendly</strong> approach helps businesses cut costs, boost efficiency, and grow without limits.</p>
|
||||
</div>
|
||||
<div>
|
||||
<h4>Follow us on</h4>
|
||||
<div class="social">
|
||||
<a href="https://www.linkedin.com/company/odoo4projects/" target="_blank" aria-label="LinkedIn"><svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M4.98 3.5a2.5 2.5 0 1 1 0 5 2.5 2.5 0 0 1 0-5zM3 9h4v12H3zM9 9h3.8v1.7h.05c.53-.95 1.83-1.95 3.76-1.95 4.02 0 4.76 2.5 4.76 5.75V21h-4v-5.5c0-1.31-.02-3-1.9-3-1.9 0-2.19 1.42-2.19 2.9V21H9z" /></svg></a>
|
||||
<a href="https://www.youtube.com/@ODOO4projects" target="_blank" aria-label="YouTube"><svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M23 12s0-3.2-.41-4.73a2.5 2.5 0 0 0-1.76-1.77C19.3 5.1 12 5.1 12 5.1s-7.3 0-8.83.4A2.5 2.5 0 0 0 1.4 7.27C1 8.8 1 12 1 12s0 3.2.41 4.73a2.5 2.5 0 0 0 1.76 1.77c1.53.4 8.83.4 8.83.4s7.3 0 8.83-.4a2.5 2.5 0 0 0 1.76-1.77C23 15.2 23 12 23 12zM9.75 15.02V8.98L15.5 12z" /></svg></a>
|
||||
</div>
|
||||
<form class="footer-contact-form" id="footerContactForm" action="mailto:support@odoo4projects.com" method="post" enctype="text/plain">
|
||||
<label class="bc-sr-only" for="footerContactEmail">Your email address</label>
|
||||
<input type="email" id="footerContactEmail" name="email" placeholder="Your email address" required />
|
||||
<div class="footer-textarea-wrap">
|
||||
<label class="bc-sr-only" for="footerContactText">Your message</label>
|
||||
<textarea id="footerContactText" name="message" placeholder="Your message..." required></textarea>
|
||||
<button type="submit" class="footer-send-btn" aria-label="Send message"><svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M2 21l21-9L2 3v7l15 2-15 2z" /></svg></button>
|
||||
</div>
|
||||
<div class="footer-form-status" id="footerFormStatus" role="status"></div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="fcopy">Copyright © ODOO4projects LLC</div>
|
||||
</div>
|
||||
</footer>
|
||||
<script defer src="/static/lang.js?v=3ec49a7e62"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -25,7 +25,7 @@
|
||||
rel="stylesheet"
|
||||
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css"
|
||||
/>
|
||||
<link rel="stylesheet" href="style.css" />
|
||||
<link rel="stylesheet" href="style.css?v=061517baef" />
|
||||
<style>
|
||||
.legal-hero {
|
||||
padding: 4rem 0 2.5rem;
|
||||
@@ -111,6 +111,7 @@
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<div class="container">
|
||||
<div class="blog-header">
|
||||
<a href="index.html">← Back to Home</a>
|
||||
@@ -121,7 +122,7 @@
|
||||
<div class="container">
|
||||
<h1>Privacy Policy</h1>
|
||||
<p style="margin-top: 0.5rem; color: rgba(33, 37, 41, 0.6); font-size: 0.85rem;">
|
||||
Last updated 2026-10-21
|
||||
Last updated 2026-11-02
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
@@ -135,7 +136,7 @@
|
||||
30 N Gould St, STE R, Sheridan<br />
|
||||
WY 82801, USA
|
||||
</p>
|
||||
<p>Contact: <span class="contact-email">support@ODOO4projects.com</span></p>
|
||||
<p>Contact: <span class="contact-email">support@odoo4projects.com</span></p>
|
||||
</div>
|
||||
|
||||
<div class="legal-card">
|
||||
@@ -173,6 +174,14 @@
|
||||
<p>Our pages load Stripe (payments), Google Fonts, Font Awesome, GSAP, and our own agent widget from our server. Like any website, these requests can see the IP address your browser connects from.</p>
|
||||
</div>
|
||||
|
||||
<div class="legal-card">
|
||||
<h2>Advertising cookies</h2>
|
||||
<p>On our Black Friday page we would like to measure our advertising with Google Ads. This is optional, and it is off until you turn it on.</p>
|
||||
<p>If you accept, the Google Ads tag loads and sets advertising cookies (named <strong>_gcl_au</strong> and <strong>_gcl_aw</strong>). They let Google tell us which ad or search led to a visit, so we spend our budget on the ads that work. They do not change what you pay, and we do not sell your data.</p>
|
||||
<p>If you decline, the tag does not load and those cookies are not set. The page works the same either way. You can change your choice at any time with the “Cookie settings” link in the footer of the Black Friday page, or by clearing cookies in your browser.</p>
|
||||
<p>Our privacy-friendly analytics are separate: they set no cookies and collect no personal data.</p>
|
||||
</div>
|
||||
|
||||
<div class="legal-card">
|
||||
<h2>Email</h2>
|
||||
<p>We email you about your account, your invoices and your service. Marketing email carries an unsubscribe link, and unsubscribing does not stop service email about an active account.</p>
|
||||
@@ -185,7 +194,7 @@
|
||||
|
||||
<div class="legal-card">
|
||||
<h2>Your requests</h2>
|
||||
<p>Ask us for a copy of your data, a correction, or deletion at <span class="contact-email">support@ODOO4projects.com</span>. If your account is active, deletion ends the service. We keep invoice records for as long as accounting rules require them.</p>
|
||||
<p>Ask us for a copy of your data, a correction, or deletion at <span class="contact-email">support@odoo4projects.com</span>. If your account is active, deletion ends the service. We keep invoice records for as long as accounting rules require them.</p>
|
||||
</div>
|
||||
|
||||
<div class="legal-card">
|
||||
@@ -195,6 +204,7 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<script defer src="/static/agent-odoo4projects/agent.js"></script>
|
||||
</main>
|
||||
<script defer src="/static/agent-odoo4projects/agent.js?v=c3bb0cee2b"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -66,6 +66,12 @@
|
||||
<changefreq>monthly</changefreq>
|
||||
<priority>0.6</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://odoo4projects.com/odoo-community-developers/</loc>
|
||||
<lastmod>2026-11-03</lastmod>
|
||||
<changefreq>monthly</changefreq>
|
||||
<priority>0.6</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://odoo4projects.com/support.html</loc>
|
||||
<lastmod>2026-10-27</lastmod>
|
||||
@@ -78,6 +84,30 @@
|
||||
<changefreq>yearly</changefreq>
|
||||
<priority>0.4</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://odoo4projects.com/blog/posts/your-agent-can-run-your-odoo-hosting.html</loc>
|
||||
<lastmod>2026-11-05</lastmod>
|
||||
<changefreq>monthly</changefreq>
|
||||
<priority>0.6</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://odoo4projects.com/blog/posts/freelancer-directory-soft-launch.html</loc>
|
||||
<lastmod>2026-11-04</lastmod>
|
||||
<changefreq>monthly</changefreq>
|
||||
<priority>0.6</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://odoo4projects.com/blog/posts/odoo-20-is-live-on-our-servers-here-is-what-changed.html</loc>
|
||||
<lastmod>2026-11-03</lastmod>
|
||||
<changefreq>monthly</changefreq>
|
||||
<priority>0.6</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://odoo4projects.com/blog/posts/from-a-spreadsheet-to-a-running-erp.html</loc>
|
||||
<lastmod>2026-11-02</lastmod>
|
||||
<changefreq>monthly</changefreq>
|
||||
<priority>0.6</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://odoo4projects.com/blog/posts/oca-paraguay-internationalization-hackathon.html</loc>
|
||||
<lastmod>2026-10-29</lastmod>
|
||||
|
||||
@@ -0,0 +1,234 @@
|
||||
/* ads-consent.js — Google Ads tag, gated behind a consent banner. Page-side only.
|
||||
*
|
||||
* Page: /odoo-black-friday/. Companion to /static/black-friday.js (which is untouched).
|
||||
* The tag ID and the conversion label are read from the banner element's data-* attributes
|
||||
* (#adsConsent[data-ads-id][data-ads-send-to]), so this file stays generic and reusable and
|
||||
* the page holds the one copy of the config. Without those attributes the script stays inert.
|
||||
*
|
||||
* WHY A GATE: the Google Ads tag is NOT cookieless — it sets the advertising cookies
|
||||
* _gcl_au / _gcl_aw. So nothing loads and no request fires until the visitor accepts.
|
||||
* The default is DENY. Declining genuinely prevents the tag from loading.
|
||||
*
|
||||
* WHAT THE CONVERSIONS ARE: both are CLICK PROXIES, not sales.
|
||||
* - a buy form on this page (POST /shop/post_basket) -> data-ads-send-to
|
||||
* - the free-trial form (#trialForm, the n8n webhook) -> data-ads-trial-send-to
|
||||
* The real purchase is measured on the checkout host (20.odoo4projects.com) by odoo-dev.
|
||||
* See wiki/concepts/conversion-tracking-contract. Never label either a sale.
|
||||
*
|
||||
* SAFETY: a conversion tag must never swallow an action. On submit we fire the event and run the
|
||||
* action from the event callback, with a hard timeout that runs it anyway. For a buy form the
|
||||
* action is form.submit(); for the trial form the page hands us its send function via
|
||||
* window.o4pAds.trialConversion. With JavaScript off the native POST forms submit untouched
|
||||
* (no banner, no conversion, no error) and the trial form does its native thing.
|
||||
*/
|
||||
(function () {
|
||||
"use strict";
|
||||
|
||||
var STORE_KEY = "o4p_ads_consent"; // "granted" | "denied"
|
||||
var SUBMIT_FALLBACK_MS = 700; // hard cap: submit even if the tag never answers
|
||||
var cfg = null; // { id, sendTo, trialSendTo } — from the banner, never hard-coded
|
||||
|
||||
/* ---- the visitor's stored choice ---- */
|
||||
function readChoice() {
|
||||
try {
|
||||
return window.localStorage.getItem(STORE_KEY);
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
function saveChoice(value) {
|
||||
try {
|
||||
window.localStorage.setItem(STORE_KEY, value);
|
||||
} catch (e) {
|
||||
/* private mode / storage disabled: treat as no choice, banner stays up */
|
||||
}
|
||||
}
|
||||
function isGranted() {
|
||||
return readChoice() === "granted";
|
||||
}
|
||||
|
||||
/* ---- cookie removal (only used to revoke a prior grant) ---- */
|
||||
function clearCookie(name) {
|
||||
var host = window.location.hostname;
|
||||
var bare = host.replace(/^www\./, "");
|
||||
var paths = ["/", "/odoo-black-friday/"];
|
||||
var domains = ["", "; domain=" + host, "; domain=." + bare];
|
||||
for (var i = 0; i < paths.length; i++) {
|
||||
for (var j = 0; j < domains.length; j++) {
|
||||
document.cookie =
|
||||
name + "=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=" + paths[i] + domains[j];
|
||||
}
|
||||
}
|
||||
}
|
||||
function clearAdsCookies() {
|
||||
clearCookie("_gcl_au");
|
||||
clearCookie("_gcl_aw");
|
||||
}
|
||||
|
||||
/* ---- the tag: loaded ONLY on consent ---- */
|
||||
function loadGtag() {
|
||||
if (!cfg || window.__o4pAdsLoaded) return;
|
||||
window.__o4pAdsLoaded = true;
|
||||
window.dataLayer = window.dataLayer || [];
|
||||
window.gtag =
|
||||
window.gtag ||
|
||||
function () {
|
||||
window.dataLayer.push(arguments);
|
||||
};
|
||||
window.gtag("js", new Date());
|
||||
window.gtag("config", cfg.id);
|
||||
var s = document.createElement("script");
|
||||
s.async = true;
|
||||
s.src = "https://www.googletagmanager.com/gtag/js?id=" + cfg.id;
|
||||
document.head.appendChild(s);
|
||||
}
|
||||
|
||||
/* ---- fire one conversion, then run the action from the callback or the hard timeout ---- */
|
||||
function fireConversion(sendTo, action) {
|
||||
var done = false;
|
||||
function run() {
|
||||
if (done) return;
|
||||
done = true;
|
||||
action(); // the action always runs: at most SUBMIT_FALLBACK_MS after the event
|
||||
}
|
||||
try {
|
||||
window.gtag("event", "conversion", {
|
||||
send_to: sendTo,
|
||||
event_callback: run,
|
||||
});
|
||||
} catch (err) {
|
||||
run();
|
||||
}
|
||||
window.setTimeout(run, SUBMIT_FALLBACK_MS);
|
||||
}
|
||||
|
||||
/* ---- the conversion proxy, fired on a buy-form submit ---- */
|
||||
/* NOTE: the buy path below is deliberately left byte-identical to what shipped and was
|
||||
* verified on 2026-11-02 — its label, its one mark and its 700 ms fallback are unchanged. */
|
||||
function onSubmit(e) {
|
||||
var form = e.target;
|
||||
if (!form || form.tagName !== "FORM") return;
|
||||
var action = form.getAttribute("action") || "";
|
||||
if (action.indexOf("/shop/post_basket") === -1) return;
|
||||
|
||||
// Not configured, no consent, or no tag: do nothing, let the native POST go through.
|
||||
if (!cfg || !isGranted() || typeof window.gtag !== "function") return;
|
||||
|
||||
e.preventDefault();
|
||||
var submitted = false;
|
||||
function submitNow() {
|
||||
if (submitted) return;
|
||||
submitted = true;
|
||||
form.submit(); // bypasses the submit event: no loop, no re-fire
|
||||
}
|
||||
try {
|
||||
window.gtag("event", "conversion", {
|
||||
send_to: cfg.sendTo,
|
||||
event_callback: submitNow,
|
||||
});
|
||||
} catch (err) {
|
||||
submitNow();
|
||||
}
|
||||
// Hard fallback: a blocked / slow / failed tag must never stop the purchase.
|
||||
window.setTimeout(submitNow, SUBMIT_FALLBACK_MS);
|
||||
}
|
||||
|
||||
/* ---- the banner ---- */
|
||||
function bannerEl() {
|
||||
return document.getElementById("adsConsent");
|
||||
}
|
||||
function showBanner() {
|
||||
var el = bannerEl();
|
||||
if (el) el.hidden = false;
|
||||
}
|
||||
function hideBanner() {
|
||||
var el = bannerEl();
|
||||
if (el) el.hidden = true;
|
||||
}
|
||||
function accept() {
|
||||
saveChoice("granted");
|
||||
loadGtag();
|
||||
hideBanner();
|
||||
}
|
||||
function decline() {
|
||||
saveChoice("denied");
|
||||
clearAdsCookies();
|
||||
hideBanner();
|
||||
}
|
||||
|
||||
function init() {
|
||||
document.addEventListener("submit", onSubmit, false);
|
||||
|
||||
var banner = bannerEl();
|
||||
if (!banner) return;
|
||||
|
||||
// Config lives in the page. Missing config -> stay inert (no tag, no conversion).
|
||||
var id = banner.getAttribute("data-ads-id");
|
||||
var sendTo = banner.getAttribute("data-ads-send-to");
|
||||
if (!id || !sendTo) return;
|
||||
cfg = {
|
||||
id: id,
|
||||
sendTo: sendTo,
|
||||
trialSendTo: banner.getAttribute("data-ads-trial-send-to"),
|
||||
bookSendTo: banner.getAttribute("data-ads-book-send-to"),
|
||||
};
|
||||
|
||||
var choice = readChoice();
|
||||
if (choice === "granted") {
|
||||
loadGtag(); // returning visitor who already accepted: load straight away
|
||||
} else if (choice !== "denied") {
|
||||
showBanner(); // first visit (or no stored choice): default deny, ask
|
||||
}
|
||||
|
||||
var acceptBtn = document.getElementById("adsConsentAccept");
|
||||
if (acceptBtn) acceptBtn.addEventListener("click", accept);
|
||||
var declineBtn = document.getElementById("adsConsentDecline");
|
||||
if (declineBtn) declineBtn.addEventListener("click", decline);
|
||||
|
||||
// A real way to change the choice later.
|
||||
var opener = document.getElementById("adsConsentOpen");
|
||||
if (opener) {
|
||||
opener.addEventListener("click", function (e) {
|
||||
e.preventDefault();
|
||||
showBanner();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- the trial conversion: the page hands us its real submit function ---- *
|
||||
* Called by /static/black-friday.js when #trialForm is submitted. With consent we fire the Ads
|
||||
* conversion and run the page's submit from the callback; a blocked/slow tag is covered by the
|
||||
* hard timeout, so the trial always completes. Without consent (or without the tag) we run the
|
||||
* submit immediately and fire nothing — the trial is never gated on the tag.
|
||||
*/
|
||||
window.o4pAds = window.o4pAds || {};
|
||||
window.o4pAds.trialConversion = function (action) {
|
||||
if (typeof action !== "function") return;
|
||||
if (!cfg || !cfg.trialSendTo || !isGranted() || typeof window.gtag !== "function") {
|
||||
action();
|
||||
return;
|
||||
}
|
||||
fireConversion(cfg.trialSendTo, action);
|
||||
};
|
||||
|
||||
/* ---- the booking conversion: fired on a click of the booking link ---- *
|
||||
* The booking happens on Google's domain, off-site, so a click is all we can observe. The
|
||||
* caller does NOT preventDefault: the link opens in a new tab on its own, so even a blocked,
|
||||
* slow or absent tag can never stop the booking page from opening — this only ADDS the event.
|
||||
* Without consent (or the label) it fires nothing and simply runs the (optional) action.
|
||||
*/
|
||||
window.o4pAds.bookConversion = function (action) {
|
||||
if (typeof action !== "function") action = function () {};
|
||||
if (!cfg || !cfg.bookSendTo || !isGranted() || typeof window.gtag !== "function") {
|
||||
action();
|
||||
return;
|
||||
}
|
||||
fireConversion(cfg.bookSendTo, action);
|
||||
};
|
||||
|
||||
if (document.readyState === "loading") {
|
||||
document.addEventListener("DOMContentLoaded", init);
|
||||
} else {
|
||||
init();
|
||||
}
|
||||
})();
|
||||
@@ -0,0 +1,521 @@
|
||||
/* Page-local script for /odoo-black-friday/.
|
||||
The homepage behaviours this landing page needs, copied from the site's /script.js:
|
||||
the hero slide-in image driven by the server-location selector, the Buy Now buttons,
|
||||
the free-trial card, the Custom Quote modal and the footer contact form.
|
||||
The blog, OCA-modules and project-plan blocks of /script.js are removed here because this
|
||||
page does not carry those sections (and those blocks throw on a page without their markup).
|
||||
Image paths are root-relative: this page lives in a subdirectory (/odoo-black-friday/).
|
||||
ES5, no build step, no new dependency beyond the gsap the homepage already loads. */
|
||||
(function () {
|
||||
"use strict";
|
||||
|
||||
/* ---- UTM tracking helpers (verbatim from /script.js) ---- */
|
||||
(function () {
|
||||
function getCookie(name) {
|
||||
var match = document.cookie.match(new RegExp('(^| )' + name + '=([^;]+)'));
|
||||
return match ? decodeURIComponent(match[2]) : null;
|
||||
}
|
||||
function setCookie(name, value, days) {
|
||||
var expires = '';
|
||||
if (days) {
|
||||
var date = new Date();
|
||||
date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000));
|
||||
expires = '; expires=' + date.toUTCString();
|
||||
}
|
||||
document.cookie = name + '=' + encodeURIComponent(value) + expires + '; path=/; SameSite=Lax';
|
||||
}
|
||||
function parseUTM() {
|
||||
var urlParams = new URLSearchParams(window.location.search);
|
||||
var utm = {};
|
||||
var keys = ['source', 'medium', 'campaign'];
|
||||
for (var i = 0; i < keys.length; i++) {
|
||||
var val = urlParams.get('utm_' + keys[i]);
|
||||
if (val) {
|
||||
utm[keys[i]] = val;
|
||||
setCookie('utm_' + keys[i], val, 90);
|
||||
}
|
||||
}
|
||||
return utm;
|
||||
}
|
||||
function getStoredUTM() {
|
||||
var utm = {};
|
||||
var keys = ['source', 'medium', 'campaign'];
|
||||
for (var i = 0; i < keys.length; i++) {
|
||||
var val = getCookie('utm_' + keys[i]);
|
||||
if (val) utm[keys[i]] = val;
|
||||
}
|
||||
return utm;
|
||||
}
|
||||
function injectUTM(data) {
|
||||
var stored = getStoredUTM();
|
||||
if (Object.keys(stored).length > 0) {
|
||||
data.utm_source = stored.source || null;
|
||||
data.utm_medium = stored.medium || null;
|
||||
data.utm_campaign = stored.campaign || null;
|
||||
}
|
||||
return data;
|
||||
}
|
||||
function appendToForm(form) {
|
||||
var stored = getStoredUTM();
|
||||
var keys = ['source', 'medium', 'campaign'];
|
||||
for (var i = 0; i < keys.length; i++) {
|
||||
if (stored[keys[i]]) {
|
||||
var input = document.createElement('input');
|
||||
input.type = 'hidden';
|
||||
input.name = 'utm_' + keys[i];
|
||||
input.value = stored[keys[i]];
|
||||
form.appendChild(input);
|
||||
}
|
||||
}
|
||||
}
|
||||
function toQuery() {
|
||||
var stored = getStoredUTM();
|
||||
var parts = [];
|
||||
var keys = ['source', 'medium', 'campaign'];
|
||||
for (var i = 0; i < keys.length; i++) {
|
||||
if (stored[keys[i]]) {
|
||||
parts.push('utm_' + keys[i] + '=' + encodeURIComponent(stored[keys[i]]));
|
||||
}
|
||||
}
|
||||
return parts.join('&');
|
||||
}
|
||||
window.UTM = { parse: parseUTM, getStored: getStoredUTM, inject: injectUTM, appendToForm: appendToForm, toQuery: toQuery };
|
||||
window.UTM.parse();
|
||||
})();
|
||||
|
||||
/* ---- Hero slide-in image + server-location selector (homepage behaviour) ---- */
|
||||
(function () {
|
||||
var SERVERS = [
|
||||
{ host: 'api.meppel.odoo4projects.com', city: 'Amsterdam' },
|
||||
{ host: 'api.boston.odoo4projects.com', city: 'Boston' },
|
||||
{ host: 'api.manchester.odoo4projects.com', city: 'Manchester' },
|
||||
{ host: 'api.mumbai.odoo4projects.com', city: 'Mumbai' },
|
||||
{ host: 'api.saopaulo.odoo4projects.com', city: 'S\u00e3o Paulo' },
|
||||
{ host: 'api.sydney.odoo4projects.com', city: 'Sydney' }
|
||||
];
|
||||
/* Root-relative on purpose: this page is served from /odoo-black-friday/. */
|
||||
var heroImages = {
|
||||
'api.meppel.odoo4projects.com': '/images/hero/amsterdam.webp',
|
||||
'api.boston.odoo4projects.com': '/images/hero/boston.webp',
|
||||
'api.manchester.odoo4projects.com': '/images/hero/london.webp',
|
||||
'api.mumbai.odoo4projects.com': '/images/hero/mumbai.webp',
|
||||
'api.saopaulo.odoo4projects.com': '/images/hero/brazil.webp',
|
||||
'api.sydney.odoo4projects.com': '/images/hero/sydney.webp'
|
||||
};
|
||||
|
||||
function pingServer(host) {
|
||||
return new Promise(function (resolve) {
|
||||
var start = performance.now();
|
||||
var img = new Image();
|
||||
var timedOut = false;
|
||||
var timer = setTimeout(function () {
|
||||
timedOut = true; img.src = '';
|
||||
resolve({ host: host, time: -1 });
|
||||
}, 2000);
|
||||
img.onload = function () {
|
||||
var elapsed = performance.now() - start;
|
||||
if (!timedOut) { clearTimeout(timer); resolve({ host: host, time: elapsed }); }
|
||||
};
|
||||
img.onerror = function () {
|
||||
var elapsed = performance.now() - start;
|
||||
if (!timedOut) { clearTimeout(timer); resolve({ host: host, time: elapsed }); }
|
||||
};
|
||||
img.src = 'https://' + host + '/favicon.ico?_=' + Date.now() + '_' + Math.random().toString(36).slice(2, 6);
|
||||
});
|
||||
}
|
||||
|
||||
function animateHero(city, image, showLabel) {
|
||||
var hero = document.querySelector('.hero');
|
||||
if (!hero || !window.gsap) return;
|
||||
var bg = hero.querySelector('.hero-bg');
|
||||
if (!bg) {
|
||||
bg = document.createElement('div');
|
||||
bg.className = 'hero-bg';
|
||||
hero.insertBefore(bg, hero.firstChild);
|
||||
}
|
||||
var hasExisting = bg.children.length > 0;
|
||||
var newEl = document.createElement('div');
|
||||
newEl.className = 'hero-bg-slide';
|
||||
newEl.style.backgroundImage = "url('" + image + "')";
|
||||
bg.appendChild(newEl);
|
||||
|
||||
var tl = gsap.timeline();
|
||||
gsap.set(newEl, { x: '100%' });
|
||||
|
||||
var label = null;
|
||||
if (showLabel !== false && city) {
|
||||
var oldLabel = hero.querySelector('.hero-city-label');
|
||||
if (oldLabel) oldLabel.remove();
|
||||
label = document.createElement('div');
|
||||
label.className = 'hero-city-label';
|
||||
label.innerHTML = 'Our <strong>' + city + '</strong> server fits your location';
|
||||
hero.appendChild(label);
|
||||
gsap.set(label, { x: '150%' });
|
||||
}
|
||||
|
||||
if (hasExisting) {
|
||||
var oldEl = bg.children[0];
|
||||
gsap.set(oldEl, { x: '0%' });
|
||||
tl.to(oldEl, { x: '100%', duration: 3, ease: 'power2.out' }, 0)
|
||||
.to(newEl, { x: '0%', duration: 3, ease: 'power2.out' }, 0);
|
||||
if (label) {
|
||||
tl.to(label, { x: '0%', duration: 2.5, ease: 'power2.out' }, 0.3);
|
||||
}
|
||||
tl.call(function () { oldEl.remove(); });
|
||||
} else {
|
||||
tl.to(newEl, { x: '0%', duration: 3, ease: 'power2.out' }, 0);
|
||||
if (label) {
|
||||
tl.to(label, { x: '0%', duration: 2.5, ease: 'power2.out' }, 0.3);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Location selector: click a server card to slide its city into the hero. */
|
||||
var serverCards = document.querySelectorAll('.server-card');
|
||||
serverCards.forEach(function (card, idx) {
|
||||
card.addEventListener('click', function () {
|
||||
var s = SERVERS[idx];
|
||||
if (!s || !heroImages[s.host]) return;
|
||||
serverCards.forEach(function (c) { c.classList.remove('server-best'); });
|
||||
card.classList.add('server-best');
|
||||
animateHero(null, heroImages[s.host], false);
|
||||
});
|
||||
});
|
||||
|
||||
/* Populate the trial + pricing location selects. */
|
||||
var trialLoc = document.getElementById('trialLocation');
|
||||
var pricingLoc = document.getElementById('pricingServerLocation');
|
||||
[trialLoc, pricingLoc].forEach(function (el) {
|
||||
if (el) {
|
||||
SERVERS.forEach(function (s) {
|
||||
var opt = document.createElement('option');
|
||||
var cityVal = s.host.replace('api.', '').replace('.odoo4projects.com', '');
|
||||
opt.value = cityVal;
|
||||
opt.textContent = s.city;
|
||||
el.appendChild(opt);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/* Ping all servers, highlight the fastest, slide its image in. */
|
||||
var results = [];
|
||||
var pending = SERVERS.length;
|
||||
SERVERS.forEach(function (s) {
|
||||
pingServer(s.host).then(function (r) {
|
||||
results.push(r);
|
||||
pending--;
|
||||
if (pending !== 0) return;
|
||||
var valid = results.filter(function (x) { return x.time >= 0; });
|
||||
if (valid.length === 0) return;
|
||||
valid.sort(function (a, b) { return a.time - b.time; });
|
||||
var best = valid[0];
|
||||
var bestServer = SERVERS.filter(function (x) { return x.host === best.host; })[0];
|
||||
if (bestServer) {
|
||||
[trialLoc, pricingLoc].forEach(function (el) {
|
||||
if (el) {
|
||||
var cityVal = bestServer.host.replace('api.', '').replace('.odoo4projects.com', '');
|
||||
for (var i = 0; i < el.options.length; i++) {
|
||||
if (el.options[i].value === cityVal) { el.selectedIndex = i; break; }
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
if (bestServer && heroImages[best.host]) {
|
||||
var cards = document.querySelectorAll('.server-card');
|
||||
for (var i = 0; i < SERVERS.length && i < cards.length; i++) {
|
||||
if (SERVERS[i].host === best.host) { cards[i].classList.add('server-best'); break; }
|
||||
}
|
||||
var img = new Image();
|
||||
img.onload = function () { animateHero(bestServer.city, heroImages[best.host]); };
|
||||
img.src = heroImages[best.host];
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
/* Free-trial card. */
|
||||
var trialForm = document.getElementById('trialForm');
|
||||
if (trialForm) {
|
||||
trialForm.addEventListener('submit', function (e) {
|
||||
e.preventDefault();
|
||||
var email = document.getElementById('trialEmail').value.trim();
|
||||
var location = trialLoc ? trialLoc.value : '';
|
||||
if (!email) return;
|
||||
var btn = trialForm.querySelector('.btn-brass');
|
||||
|
||||
function sendTrial() {
|
||||
btn.textContent = 'Sending...';
|
||||
btn.disabled = true;
|
||||
fetch('https://n8n.odoo4projects.com/webhook/ODOO4projects/trial', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(UTM.inject({ email: email, location: location }))
|
||||
}).then(function () {
|
||||
if (window.plausible) window.plausible('Trial');
|
||||
trialForm.style.display = 'none';
|
||||
document.getElementById('trialSuccess').classList.add('visible');
|
||||
}).catch(function () {
|
||||
btn.textContent = 'Send';
|
||||
btn.disabled = false;
|
||||
var errorMsg = document.createElement('div');
|
||||
errorMsg.className = 'trial-error visible';
|
||||
errorMsg.textContent = 'Something went wrong. Please try again.';
|
||||
trialForm.appendChild(errorMsg);
|
||||
setTimeout(function () { errorMsg.classList.remove('visible'); }, 3000);
|
||||
});
|
||||
}
|
||||
|
||||
/* When the consent gate is present it fires the Google Ads trial conversion and runs
|
||||
sendTrial from the tag's callback, with a hard timeout that runs it anyway — so a
|
||||
blocked or slow tag can never swallow the trial. Without the gate, send directly. */
|
||||
if (window.o4pAds && typeof window.o4pAds.trialConversion === 'function') {
|
||||
window.o4pAds.trialConversion(sendTrial);
|
||||
} else {
|
||||
sendTrial();
|
||||
}
|
||||
});
|
||||
}
|
||||
})();
|
||||
|
||||
/* ---- Buy Now — POST the basket straight to the shop, open checkout in a new tab ---- */
|
||||
window.buyNow = function (productId) {
|
||||
var locSelect = document.getElementById('pricingServerLocation');
|
||||
var location = locSelect ? locSelect.value : '';
|
||||
var verSelect = document.getElementById('pricingOdooVersion');
|
||||
var version = verSelect ? verSelect.value : '';
|
||||
var form = document.createElement('form');
|
||||
form.method = 'POST';
|
||||
form.action = 'https://20.odoo4projects.com/shop/post_basket';
|
||||
form.target = '_blank';
|
||||
form.style.display = 'none';
|
||||
var basket = document.createElement('input');
|
||||
basket.type = 'hidden';
|
||||
basket.name = 'basket';
|
||||
var item = { product_id: productId, qty: 1 };
|
||||
var info = location ? location.replace('.odoo4projects.com', '') : '';
|
||||
if (version) { info += (info ? '|' : '') + version; }
|
||||
if (info) { item.info = info; }
|
||||
basket.value = JSON.stringify([item]);
|
||||
form.appendChild(basket);
|
||||
var clear = document.createElement('input');
|
||||
clear.type = 'hidden';
|
||||
clear.name = 'clear';
|
||||
clear.value = '1';
|
||||
form.appendChild(clear);
|
||||
UTM.appendToForm(form);
|
||||
document.body.appendChild(form);
|
||||
form.submit();
|
||||
document.body.removeChild(form);
|
||||
};
|
||||
|
||||
/* ---- Carry the chosen Server Location + Odoo version into the native buy forms ----
|
||||
* The Black Friday buy buttons are plain form POSTs to /shop/post_basket, so they work with
|
||||
* JavaScript off. buyNow() builds `info` for the JS buttons; this does exactly the same for
|
||||
* the native forms, on submit, from the one global selector pair (#pricingServerLocation +
|
||||
* #pricingOdooVersion). Additive only: with JavaScript off the basket posts exactly as before
|
||||
* (no info) and the selector is empty anyway because its options are built here. The POST is
|
||||
* untouched — only the form's own hidden `basket` input is rewritten. */
|
||||
(function () {
|
||||
var forms = document.querySelectorAll('form[action*="/shop/post_basket"]');
|
||||
forms.forEach(function (form) {
|
||||
form.addEventListener('submit', function () {
|
||||
var basket = form.querySelector('input[name="basket"]');
|
||||
if (!basket) return;
|
||||
var locSelect = document.getElementById('pricingServerLocation');
|
||||
var verSelect = document.getElementById('pricingOdooVersion');
|
||||
var location = locSelect ? locSelect.value : '';
|
||||
var version = verSelect ? verSelect.value : '';
|
||||
var info = location ? location.replace('.odoo4projects.com', '') : '';
|
||||
if (version) { info += (info ? '|' : '') + version; }
|
||||
if (!info) return;
|
||||
try {
|
||||
var items = JSON.parse(basket.value);
|
||||
if (!Array.isArray(items)) return;
|
||||
items.forEach(function (it) { if (it) { it.info = info; } });
|
||||
basket.value = JSON.stringify(items);
|
||||
} catch (e) { /* leave the basket untouched rather than break the purchase */ }
|
||||
});
|
||||
});
|
||||
})();
|
||||
|
||||
/* ---- Buy buttons + Custom Quote modal ---- */
|
||||
(function () {
|
||||
document.querySelectorAll('.btn-buy').forEach(function (btn) {
|
||||
btn.addEventListener('click', function () {
|
||||
var id = this.getAttribute('data-id');
|
||||
if (id) buyNow(parseInt(id, 10));
|
||||
});
|
||||
});
|
||||
|
||||
function openCustomQuoteModal() {
|
||||
document.getElementById('customQuotePanel').style.display = 'block';
|
||||
document.getElementById('customQuoteModal').style.display = 'flex';
|
||||
}
|
||||
function closeCustomQuoteModal() {
|
||||
document.getElementById('customQuoteModal').style.display = 'none';
|
||||
document.getElementById('customQuoteForm').reset();
|
||||
document.getElementById('customQuoteSuccess').style.display = 'none';
|
||||
}
|
||||
window.openCustomQuoteModal = openCustomQuoteModal;
|
||||
window.closeCustomQuoteModal = closeCustomQuoteModal;
|
||||
|
||||
document.querySelectorAll('.btn-empire').forEach(function (btn) {
|
||||
btn.addEventListener('click', function () { openCustomQuoteModal(); });
|
||||
});
|
||||
|
||||
document.addEventListener('keydown', function (e) {
|
||||
if (e.key === 'Escape') {
|
||||
var modal = document.getElementById('customQuoteModal');
|
||||
if (modal && modal.style.display === 'flex') { closeCustomQuoteModal(); }
|
||||
}
|
||||
});
|
||||
|
||||
var customQuoteForm = document.getElementById('customQuoteForm');
|
||||
if (customQuoteForm) {
|
||||
customQuoteForm.addEventListener('submit', function (e) {
|
||||
e.preventDefault();
|
||||
var name = document.getElementById('cqName').value.trim();
|
||||
var email = document.getElementById('cqEmail').value.trim();
|
||||
var requirements = document.getElementById('cqRequirements').value.trim();
|
||||
if (!name || !email || !requirements) return;
|
||||
var submitBtn = customQuoteForm.querySelector('.btn-lg');
|
||||
submitBtn.textContent = 'Sending...';
|
||||
submitBtn.disabled = true;
|
||||
fetch('https://n8n.odoo4projects.com/webhook/ODOO4projects/crm', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(UTM.inject({ name: name, email: email, requirements: requirements, area: 'quote' }))
|
||||
}).then(function (r) {
|
||||
if (!r.ok) throw new Error('bad status ' + r.status);
|
||||
if (window.plausible) window.plausible('Custom Quote');
|
||||
customQuoteForm.style.display = 'none';
|
||||
document.getElementById('customQuoteSuccess').style.display = 'block';
|
||||
submitBtn.textContent = 'Submit Request';
|
||||
submitBtn.disabled = false;
|
||||
}).catch(function () {
|
||||
submitBtn.textContent = 'Submit Request';
|
||||
submitBtn.disabled = false;
|
||||
var errorMsg = document.createElement('div');
|
||||
errorMsg.style.marginTop = '1rem';
|
||||
errorMsg.style.color = '#dc3545';
|
||||
errorMsg.textContent = 'Something went wrong. Please try again.';
|
||||
customQuoteForm.appendChild(errorMsg);
|
||||
setTimeout(function () { if (errorMsg.parentNode) errorMsg.parentNode.removeChild(errorMsg); }, 3000);
|
||||
});
|
||||
});
|
||||
}
|
||||
})();
|
||||
|
||||
/* ---- Booking link — fire the Google Ads booking conversion on the click, through the same
|
||||
consent gate as the buy and trial conversions. The link is a plain <a target="_blank">, so
|
||||
the click is never preventDefault-ed: the Google appointment page opens regardless, and a
|
||||
blocked, slow or absent tag merely skips the event. Fires nothing without consent. ---- */
|
||||
(function () {
|
||||
var link = document.getElementById('bookingCTA');
|
||||
if (!link) return;
|
||||
link.addEventListener('click', function () {
|
||||
if (window.o4pAds && typeof window.o4pAds.bookConversion === 'function') {
|
||||
window.o4pAds.bookConversion();
|
||||
}
|
||||
});
|
||||
})();
|
||||
|
||||
/* ---- Footer contact form ---- */
|
||||
(function () {
|
||||
var form = document.getElementById('footerContactForm');
|
||||
if (!form) return;
|
||||
var emailInput = document.getElementById('footerContactEmail');
|
||||
var textInput = document.getElementById('footerContactText');
|
||||
var submitBtn = form.querySelector('.footer-send-btn');
|
||||
var status = document.getElementById('footerFormStatus');
|
||||
function showStatus(message, isError) {
|
||||
status.textContent = message;
|
||||
status.classList.toggle('error', !!isError);
|
||||
status.classList.add('visible');
|
||||
}
|
||||
form.addEventListener('submit', function (e) {
|
||||
e.preventDefault();
|
||||
var email = emailInput.value.trim();
|
||||
var text = textInput.value.trim();
|
||||
if (!email || !text) return;
|
||||
submitBtn.disabled = true;
|
||||
status.classList.remove('visible');
|
||||
fetch('https://n8n.odoo4projects.com/webhook/ODOO4projects/crm', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(UTM.inject({ email: email, requirements: text, area: 'message' }))
|
||||
}).then(function (r) {
|
||||
if (!r.ok) throw new Error('bad status ' + r.status);
|
||||
form.reset();
|
||||
submitBtn.disabled = false;
|
||||
showStatus("Thanks! We'll get back to you shortly.", false);
|
||||
}).catch(function () {
|
||||
submitBtn.disabled = false;
|
||||
showStatus('Something went wrong. Please try again.', true);
|
||||
});
|
||||
});
|
||||
})();
|
||||
|
||||
/* ---- Scroll-triggered reveal (same grids as the homepage, minus blog / project-plan) ---- */
|
||||
(function () {
|
||||
if (!window.gsap || !('IntersectionObserver' in window)) return;
|
||||
var animated = {};
|
||||
var observer = new IntersectionObserver(function (entries) {
|
||||
entries.forEach(function (entry) {
|
||||
if (!entry.isIntersecting) return;
|
||||
var grid = entry.target;
|
||||
if (!grid || !grid.children || grid.children.length === 0) return;
|
||||
var key = grid.className || 'anon';
|
||||
if (animated[key]) return;
|
||||
animated[key] = true;
|
||||
gsap.fromTo(grid.children, { y: 30, opacity: 0 }, {
|
||||
y: 0, opacity: 1, duration: 0.7, ease: 'power2.out', stagger: 0.08, overwrite: 'auto'
|
||||
});
|
||||
});
|
||||
}, { threshold: 0.1 });
|
||||
|
||||
var grids = document.querySelectorAll('.features-grid, .faq-grid, .pricing-grid');
|
||||
grids.forEach(function (g) {
|
||||
if (g.children.length > 0) { gsap.set(g.children, { y: 30, opacity: 0 }); }
|
||||
observer.observe(g);
|
||||
});
|
||||
})();
|
||||
|
||||
/* ---- Black Friday time remaining (Fix 10) ----
|
||||
ONE documented constant: the instant the offer ends — 30 November 2026, end of day, in the
|
||||
account's US/Eastern context (EST, UTC-5, after the 1 Nov DST change). The day count is
|
||||
computed client-side from this instant on every page load; it is NEVER hard-coded, so it
|
||||
stays correct as the date changes.
|
||||
|
||||
Fail-safe: the HTML carries the static sentence "Ends Monday 30 November 2026." with an empty
|
||||
span beside it, so with JavaScript off the deadline still reads correctly on its own. Once the
|
||||
end instant has passed the text switches to the past tense — it never keeps claiming time
|
||||
remains. Nothing here animates visibility. */
|
||||
(function () {
|
||||
var BF_END = new Date('2026-11-30T23:59:59-05:00');
|
||||
var DAY = 86400000;
|
||||
var nodes = document.querySelectorAll('[data-bf-days]');
|
||||
if (!nodes.length) return;
|
||||
|
||||
/* Whole calendar days in US/Eastern, so "days left" is a date difference and not a 24-hour
|
||||
window (the offer ends at the end of a day, not 28 x 24h from now). */
|
||||
function easternDayNumber(d) {
|
||||
var parts = new Intl.DateTimeFormat('en-CA', {
|
||||
timeZone: 'America/New_York', year: 'numeric', month: '2-digit', day: '2-digit'
|
||||
}).format(d).split('-');
|
||||
return Date.UTC(parseInt(parts[0], 10), parseInt(parts[1], 10) - 1, parseInt(parts[2], 10)) / DAY;
|
||||
}
|
||||
|
||||
var daysLeft = easternDayNumber(BF_END) - easternDayNumber(new Date());
|
||||
var text;
|
||||
if (daysLeft > 1) {
|
||||
text = daysLeft + ' days left';
|
||||
} else if (daysLeft === 1) {
|
||||
text = '1 day left';
|
||||
} else if (daysLeft === 0) {
|
||||
text = 'last day today';
|
||||
} else {
|
||||
text = 'this offer has ended';
|
||||
}
|
||||
nodes.forEach(function (n) { n.textContent = text; });
|
||||
})();
|
||||
})();
|
||||
@@ -0,0 +1,137 @@
|
||||
/* bootcamp-availability.js — live remaining-seat count for the bootcamp cohort.
|
||||
*
|
||||
* Source of truth: the availability webhook (product 17 = Bootcamp Q1-2027).
|
||||
* Progressive enhancement, same spirit as buy.js — it only ever ADDS, and it can
|
||||
* never block the buy:
|
||||
*
|
||||
* fetch OK, seats > 0 -> fill the [data-bc-seats] line with "<n> of 6 seats available"
|
||||
* in the page's own language; Buy stays enabled.
|
||||
* fetch OK, seats == 0 -> say the cohort is full, point at the next one, and disable
|
||||
* the Buy buttons (we must not sell a full cohort).
|
||||
* anything else -> DO NOTHING. Timeout, network error, CORS, non-200 or an
|
||||
* unexpected payload leave the page exactly as it is: the static
|
||||
* cap text shows and the Buy buttons stay enabled. A
|
||||
* conversion-critical element must not fail closed.
|
||||
*
|
||||
* The count is live and authoritative, so it is never written into the HTML as a fixed
|
||||
* string — the [data-bc-seats] element ships empty and hidden.
|
||||
*/
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
var ENDPOINT = 'https://n8n.odoo4projects.com/webhook/ODOO4projects/available';
|
||||
var PRODUCT_ID = 17;
|
||||
var CAP = 6; /* the page's own cohort cap ("Limited to 6 participants") */
|
||||
var TIMEOUT_MS = 6000;
|
||||
|
||||
/* The page's own words and language. "seat" = seat / Platz / plaza, as used elsewhere
|
||||
on the same page ("A seat can be transferred" / "Ein Platz ist übertragbar" /
|
||||
"Una plaza se puede transferir"). */
|
||||
var TEXT = {
|
||||
en: {
|
||||
available: function (n) { return n + ' of ' + CAP + ' seats available'; },
|
||||
full: 'This cohort is full.',
|
||||
next: 'Talk to us about the next cohort',
|
||||
href: '/support.html#contact'
|
||||
},
|
||||
de: {
|
||||
available: function (n) { return n + ' von ' + CAP + ' Plätzen frei'; },
|
||||
full: 'Dieser Kurs ist ausgebucht.',
|
||||
next: 'Sprechen Sie mit uns über den nächsten Kurs',
|
||||
href: '/de/support.html#contact'
|
||||
},
|
||||
es: {
|
||||
available: function (n) { return n + ' de ' + CAP + ' plazas disponibles'; },
|
||||
full: 'Este grupo está completo.',
|
||||
next: 'Hable con nosotros sobre el próximo grupo',
|
||||
href: '/es/support.html#contact'
|
||||
}
|
||||
};
|
||||
|
||||
function lang() {
|
||||
var l = (document.documentElement.getAttribute('lang') || 'en').toLowerCase();
|
||||
if (l.indexOf('de') === 0) return 'de';
|
||||
if (l.indexOf('es') === 0) return 'es';
|
||||
return 'en';
|
||||
}
|
||||
|
||||
function renderAvailable(el, n, t) {
|
||||
el.textContent = '';
|
||||
var strong = document.createElement('strong');
|
||||
strong.textContent = t.available(n);
|
||||
el.appendChild(strong);
|
||||
el.hidden = false;
|
||||
}
|
||||
|
||||
function renderFull(el, t) {
|
||||
el.textContent = '';
|
||||
var strong = document.createElement('strong');
|
||||
strong.textContent = t.full;
|
||||
el.appendChild(strong);
|
||||
el.appendChild(document.createTextNode(' '));
|
||||
var a = document.createElement('a');
|
||||
a.href = t.href;
|
||||
a.textContent = t.next;
|
||||
el.appendChild(a);
|
||||
el.hidden = false;
|
||||
disableBuy();
|
||||
}
|
||||
|
||||
function disableBuy() {
|
||||
var buttons = document.querySelectorAll('form[action$="/shop/post_basket"] button[type="submit"]');
|
||||
for (var i = 0; i < buttons.length; i++) {
|
||||
buttons[i].disabled = true;
|
||||
buttons[i].setAttribute('aria-disabled', 'true');
|
||||
}
|
||||
}
|
||||
|
||||
/* Unwrap [{data:[...]}] or {data:[...]} and read product 17's qty_available.
|
||||
Returns a finite number >= 0, or null if the shape is not what we expect. */
|
||||
function seatsFrom(json) {
|
||||
var obj = json;
|
||||
if (Array.isArray(obj)) obj = obj[0];
|
||||
if (!obj || !Array.isArray(obj.data)) return null;
|
||||
for (var i = 0; i < obj.data.length; i++) {
|
||||
var row = obj.data[i];
|
||||
if (row && Number(row.id) === PRODUCT_ID) {
|
||||
var q = Number(row.qty_available);
|
||||
return (isFinite(q) && q >= 0) ? q : null;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function run() {
|
||||
var el = document.querySelector('[data-bc-seats]');
|
||||
if (!el) return;
|
||||
if (!window.fetch || typeof AbortController === 'undefined') return; /* keep fallback */
|
||||
var t = TEXT[lang()];
|
||||
|
||||
var ctrl = new AbortController();
|
||||
var timer = setTimeout(function () { ctrl.abort(); }, TIMEOUT_MS);
|
||||
|
||||
fetch(ENDPOINT, {
|
||||
signal: ctrl.signal,
|
||||
headers: { 'Accept': 'application/json' },
|
||||
cache: 'no-store'
|
||||
})
|
||||
.then(function (r) {
|
||||
if (!r.ok) throw new Error('HTTP ' + r.status);
|
||||
return r.json();
|
||||
})
|
||||
.then(function (json) {
|
||||
var n = seatsFrom(json);
|
||||
if (n === null) return; /* unexpected payload -> keep fallback */
|
||||
if (n === 0) renderFull(el, t);
|
||||
else renderAvailable(el, n, t);
|
||||
})
|
||||
.catch(function () { /* keep fallback; Buy stays enabled */ })
|
||||
.then(function () { clearTimeout(timer); });
|
||||
}
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', run);
|
||||
} else {
|
||||
run();
|
||||
}
|
||||
})();
|
||||
@@ -0,0 +1,186 @@
|
||||
/* bootcamp-waitlist.js — page-local waiting-list form for the three bootcamp pages.
|
||||
*
|
||||
* The user's rule: "You want another timezone / language? Join the waiting list."
|
||||
* The form posts to the same n8n CRM webhook the site's other forms use, with the
|
||||
* site's own footer-message payload shape — area:'message', no `name` (that belongs to
|
||||
* the `area:'quote'` shape of the Custom Quote modal).
|
||||
*
|
||||
* This page does not load /script.js, so the UTM helpers are copied verbatim from it
|
||||
* (same as /static/black-friday.js does). If script.js's helpers change, change these.
|
||||
*
|
||||
* All copy — labels, placeholders, the "sending" label, success and error text — lives in
|
||||
* the page markup, in the page's own language. This file carries no strings.
|
||||
*
|
||||
* A 200 from the webhook means the workflow accepted the request, not that a CRM record
|
||||
* exists (the workflow answers before it runs) — same as the site's other forms.
|
||||
*/
|
||||
var ENDPOINT = 'https://n8n.odoo4projects.com/webhook/ODOO4projects/crm';
|
||||
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
/* ---- UTM tracking helpers (verbatim from /script.js) ---- */
|
||||
function getCookie(name) {
|
||||
var match = document.cookie.match(new RegExp('(^| )' + name + '=([^;]+)'));
|
||||
return match ? decodeURIComponent(match[2]) : null;
|
||||
}
|
||||
|
||||
function setCookie(name, value, days) {
|
||||
var expires = '';
|
||||
if (days) {
|
||||
var date = new Date();
|
||||
date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000));
|
||||
expires = '; expires=' + date.toUTCString();
|
||||
}
|
||||
document.cookie = name + '=' + encodeURIComponent(value) + expires + '; path=/; SameSite=Lax';
|
||||
}
|
||||
|
||||
function parseUTM() {
|
||||
var urlParams = new URLSearchParams(window.location.search);
|
||||
var utm = {};
|
||||
var keys = ['source', 'medium', 'campaign'];
|
||||
for (var i = 0; i < keys.length; i++) {
|
||||
var val = urlParams.get('utm_' + keys[i]);
|
||||
if (val) {
|
||||
utm[keys[i]] = val;
|
||||
setCookie('utm_' + keys[i], val, 90);
|
||||
}
|
||||
}
|
||||
return utm;
|
||||
}
|
||||
|
||||
function getStoredUTM() {
|
||||
var utm = {};
|
||||
var keys = ['source', 'medium', 'campaign'];
|
||||
for (var i = 0; i < keys.length; i++) {
|
||||
var val = getCookie('utm_' + keys[i]);
|
||||
if (val) utm[keys[i]] = val;
|
||||
}
|
||||
return utm;
|
||||
}
|
||||
|
||||
function injectUTM(data) {
|
||||
var stored = getStoredUTM();
|
||||
if (Object.keys(stored).length > 0) {
|
||||
data.utm_source = stored.source || null;
|
||||
data.utm_medium = stored.medium || null;
|
||||
data.utm_campaign = stored.campaign || null;
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
window.UTM = { parse: parseUTM, getStored: getStoredUTM, inject: injectUTM };
|
||||
window.UTM.parse();
|
||||
|
||||
/* ---- Waiting list ---- */
|
||||
function run() {
|
||||
var form = document.getElementById('bcWaitlistForm');
|
||||
if (!form) return;
|
||||
var emailInput = document.getElementById('bcWaitlistEmail');
|
||||
var commentInput = document.getElementById('bcWaitlistComment');
|
||||
var submitBtn = form.querySelector('button[type="submit"]');
|
||||
var success = document.getElementById('bcWaitlistSuccess');
|
||||
var error = document.getElementById('bcWaitlistError');
|
||||
var label = submitBtn ? submitBtn.textContent : '';
|
||||
|
||||
/* Both fields are required, exactly as the footer form requires both. */
|
||||
form.addEventListener('submit', function (e) {
|
||||
e.preventDefault();
|
||||
var email = emailInput.value.trim();
|
||||
var comment = commentInput.value.trim();
|
||||
if (!email || !comment) return;
|
||||
|
||||
error.style.display = 'none';
|
||||
submitBtn.textContent = submitBtn.getAttribute('data-sending') || label;
|
||||
submitBtn.disabled = true;
|
||||
|
||||
fetch(ENDPOINT, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(UTM.inject({
|
||||
email: email,
|
||||
requirements: comment,
|
||||
area: 'message'
|
||||
}))
|
||||
}).then(function (r) {
|
||||
if (!r.ok) throw new Error('bad status ' + r.status);
|
||||
if (window.plausible) window.plausible('Bootcamp Waitlist');
|
||||
form.style.display = 'none';
|
||||
success.style.display = 'block';
|
||||
}).catch(function () {
|
||||
submitBtn.textContent = label;
|
||||
submitBtn.disabled = false;
|
||||
error.style.display = 'block';
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', run);
|
||||
} else {
|
||||
run();
|
||||
}
|
||||
})();
|
||||
|
||||
/* ---- Footer contact form ------------------------------------------------------------
|
||||
* These pages do NOT load /script.js, so its footer-form handler does not run here. This
|
||||
* mirrors /script.js's handler one-for-one — same webhook, same payload shape
|
||||
* ({ email, requirements, area:'message' } wrapped in UTM.inject), same success/failure
|
||||
* behaviour — but reads its two strings from the form's own data-ok / data-err attributes
|
||||
* so the message is in the page's language. Without it a submit would navigate the visitor
|
||||
* to the webhook's raw JSON reply.
|
||||
*/
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
function runFooter() {
|
||||
var form = document.getElementById('footerContactForm');
|
||||
if (!form) return;
|
||||
var emailInput = document.getElementById('footerContactEmail');
|
||||
var textInput = document.getElementById('footerContactText');
|
||||
var submitBtn = form.querySelector('.footer-send-btn');
|
||||
var status = document.getElementById('footerFormStatus');
|
||||
var okText = form.getAttribute('data-ok') || "Thanks! We'll get back to you shortly.";
|
||||
var errText = form.getAttribute('data-err') || 'Something went wrong. Please try again.';
|
||||
|
||||
function showStatus(message, isError) {
|
||||
status.textContent = message;
|
||||
status.classList.toggle('error', !!isError);
|
||||
status.classList.add('visible');
|
||||
}
|
||||
|
||||
form.addEventListener('submit', function (e) {
|
||||
e.preventDefault();
|
||||
var email = emailInput.value.trim();
|
||||
var text = textInput.value.trim();
|
||||
if (!email || !text) return;
|
||||
|
||||
submitBtn.disabled = true;
|
||||
status.classList.remove('visible');
|
||||
|
||||
fetch(ENDPOINT, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(window.UTM.inject({
|
||||
email: email,
|
||||
requirements: text,
|
||||
area: 'message'
|
||||
}))
|
||||
}).then(function (r) {
|
||||
if (!r.ok) throw new Error('bad status ' + r.status);
|
||||
form.reset();
|
||||
submitBtn.disabled = false;
|
||||
showStatus(okText, false);
|
||||
}).catch(function () {
|
||||
submitBtn.disabled = false;
|
||||
showStatus(errText, true);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', runFooter);
|
||||
} else {
|
||||
runFooter();
|
||||
}
|
||||
})();
|
||||
@@ -1,7 +1,7 @@
|
||||
/* buy.js — page-local checkout helper (progressive enhancement).
|
||||
*
|
||||
* The checkout itself is a native <form method="post" target="_blank"> that posts
|
||||
* basket=[{"product_id":12,"qty":1}] + clear=1 to
|
||||
* basket=[{"product_id":17,"qty":1}] + clear=1 to
|
||||
* https://20.odoo4projects.com/shop/post_basket. With JavaScript off it still works.
|
||||
*
|
||||
* This file only adds attribution: on submit it appends the utm_* hidden inputs
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
/* The freelancer PHOTO — one endpoint, one constant, one fetch.
|
||||
|
||||
The photo does NOT live in the profile payload any more. It used to ride there as
|
||||
inline base64 under the key `Image`; the webhook no longer sends that key, so the
|
||||
payload is small and the photo is fetched separately, per card, by record id.
|
||||
|
||||
ONE constant holds BOTH halves of the address: the route and the query-parameter
|
||||
name. Changing either is a one-line edit here, and both pages pick it up.
|
||||
|
||||
The parameter name is `id`, CONFIRMED by measurement against the live route:
|
||||
`?id=<n>` answers 200 with the photo for that record, while `/4`, `?profile_id=`,
|
||||
`?image_id=` and a missing id all fail.
|
||||
|
||||
THE ENDPOINT RETURNS JSON, NOT IMAGE BYTES. Measured on the live route:
|
||||
|
||||
GET …/odoo_experts_image?id=10 → 200 application/json
|
||||
body: {"Image":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQ…"}
|
||||
|
||||
So a plain `<img src="…?id=N">` receives JSON, cannot decode it and fires `onerror`
|
||||
— the monogram. That is why the card FETCHES the route, reads `Image` and puts the
|
||||
DATA URI on an `<img>` only when it passes the `isImageSrc` allowlist. n8n sometimes
|
||||
answers an array instead of an object, so both shapes are accepted.
|
||||
|
||||
`load(id)` resolves to a usable image source, or `null` on ANY failure — a non-2xx, an
|
||||
empty `{}`, a missing/blank `Image`, a value that fails `isImageSrc`, or a request that
|
||||
does not answer in `timeoutMs`. A `null` answer means "show the monogram": the caller
|
||||
never sees a broken image. The decoded URI is cached by id (and a failure is remembered
|
||||
too), so a re-render can never re-fetch a photo already in memory.
|
||||
|
||||
THE TIMEOUT. A request that neither succeeds nor errors leaves the fallback unable to
|
||||
fire (measured live: an <img> pointed at this route sat `complete=false · naturalWidth=0`
|
||||
for 20 s). Each fetch is therefore bounded by `timeoutMs` (default 8000): an
|
||||
`AbortController` is aborted by a `setTimeout`, the abort rejects the fetch, and the
|
||||
rejection lands in the same catch that returns `null` — the monogram. The timer is
|
||||
cleared on every completion, so a fast answer never pays for the guard. `AbortController`
|
||||
is universal in the browsers this site targets (it already needs `fetch`, `Promise` and
|
||||
`IntersectionObserver`); if it were missing there is nothing to abort with, but the
|
||||
abort is what turns a hang into the fallback, so it is the one capability worth naming.
|
||||
|
||||
The route answers with `Access-Control-Allow-Origin` echoing the caller and
|
||||
`Cache-Control: public, max-age=3600`, so the browser may also serve a repeat from its
|
||||
own HTTP cache. */
|
||||
window.O4PExpertImage = {
|
||||
endpoint: "https://n8n.odoo4projects.com/webhook/ODOO4projects/odoo_experts_image",
|
||||
idParam: "id",
|
||||
// A photo request that hangs is a card with no photo AND no fallback: bound it.
|
||||
timeoutMs: 8000,
|
||||
|
||||
/* The ONLY allowlist for an image source in this codebase — `https://` or a
|
||||
`data:image/` URI. Both forms are what this endpoint has served across its life. */
|
||||
isImageSrc: function (v) {
|
||||
return typeof v === "string" &&
|
||||
(v.indexOf("https://") === 0 || v.indexOf("data:image/") === 0);
|
||||
},
|
||||
|
||||
url: function (id) {
|
||||
return this.endpoint + "?" + this.idParam + "=" + encodeURIComponent(id);
|
||||
},
|
||||
|
||||
// id -> decoded image source (or "" when the record has no usable photo)
|
||||
cache: Object.create(null),
|
||||
// id -> the in-flight Promise, so two observers of one id share ONE request
|
||||
inflight: Object.create(null),
|
||||
|
||||
/* Fetch the photo for `id`. Resolves to an image source, or null when there is none.
|
||||
Never rejects — a caller only has to check for a value. Every request is bounded by
|
||||
`timeoutMs`: on expiry the fetch is ABORTED (not merely abandoned) and the rejection
|
||||
resolves null, so a hung endpoint becomes the monogram instead of an empty box. */
|
||||
load: function (id) {
|
||||
var self = this;
|
||||
var key = String(id);
|
||||
if (key in self.cache) return Promise.resolve(self.cache[key] || null);
|
||||
if (self.inflight[key]) return self.inflight[key];
|
||||
|
||||
var ctrl = (typeof AbortController !== "undefined") ? new AbortController() : null;
|
||||
var timer = ctrl ? setTimeout(function () { ctrl.abort(); }, self.timeoutMs) : 0;
|
||||
|
||||
var req = fetch(self.url(key), {
|
||||
headers: { Accept: "application/json" },
|
||||
signal: ctrl ? ctrl.signal : undefined
|
||||
})
|
||||
.then(function (res) {
|
||||
if (!res.ok) throw new Error("HTTP " + res.status);
|
||||
return res.json();
|
||||
})
|
||||
.then(function (body) {
|
||||
// n8n sometimes wraps the record in an array — accept both shapes.
|
||||
var d = Array.isArray(body) ? body[0] : body;
|
||||
var img = d && d.Image;
|
||||
if (!self.isImageSrc(img)) throw new Error("no image");
|
||||
self.cache[key] = img;
|
||||
return img;
|
||||
})
|
||||
.catch(function () {
|
||||
// Every failure — non-2xx, no image, a decode-hostile value, or the timeout's
|
||||
// abort — is the monogram. Remember it, so a shared id under a re-render does not
|
||||
// ask the network twice.
|
||||
self.cache[key] = "";
|
||||
return null;
|
||||
})
|
||||
.then(function (out) {
|
||||
if (timer) clearTimeout(timer);
|
||||
delete self.inflight[key];
|
||||
return out;
|
||||
});
|
||||
|
||||
self.inflight[key] = req;
|
||||
return req;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,22 @@
|
||||
/* The ONE source of truth for the "skill profile" rule — the 15-point budget.
|
||||
Two forms render this rule: the directory application form
|
||||
(/odoo-community-developers/) and the private self-service edit form
|
||||
(/freelancer-edit/). Both take the legend, the directive and the wrong-total
|
||||
message from here, so the two cannot word the same rule differently again.
|
||||
Loaded before each page's own script. */
|
||||
(function () {
|
||||
var TOTAL = 15;
|
||||
window.O4PFocus = {
|
||||
TOTAL: TOTAL,
|
||||
MAX: 5,
|
||||
LEGEND: "Skill profile — 15 points to distribute",
|
||||
DIRECTIVE: "Distribute all 15 points across the five areas — they cannot all be 0.",
|
||||
/* The wrong-total message, naming the direction to move. "" when the sum is right. */
|
||||
message: function (sum) {
|
||||
if (sum === TOTAL) return "";
|
||||
if (sum === 0) return "You have 0 of " + TOTAL + " — place all " + TOTAL + " points.";
|
||||
if (sum > TOTAL) return "You have " + sum + " of " + TOTAL + " — remove " + (sum - TOTAL) + ".";
|
||||
return "You have " + sum + " of " + TOTAL + " — add " + (TOTAL - sum) + ".";
|
||||
}
|
||||
};
|
||||
})();
|
||||
@@ -0,0 +1,22 @@
|
||||
# static/icons — the profile-link icon set
|
||||
|
||||
Four monochrome brand marks shown in the freelancer directory card's links row
|
||||
(`/odoo-community-developers/`): LinkedIn, GitHub, Upwork, Fiverr.
|
||||
|
||||
**Source:** [Simple Icons](https://simpleicons.org) v13 — one uniform set, so the four
|
||||
share one visual language by construction: the same 24 × 24 grid, the same internal
|
||||
padding, the same monochrome mass, single-path art. Nothing here is hand-drawn and
|
||||
nothing is fetched from a third party at runtime.
|
||||
|
||||
**How the page uses them.** The card inlines the same path data (see `LINK_ICONS` in the
|
||||
page's script and in `work/build-directory.py`), so the links row makes **zero requests**
|
||||
and each mark inherits the link's `currentColor`. These files are the self-hosted,
|
||||
drop-in copy of that set — identical geometry — for any host that prefers a file over an
|
||||
inline `<svg>`.
|
||||
|
||||
**Brand rule.** A brand mark is never redrawn and never restyled beyond a single
|
||||
monochrome fill. The one styling the page applies is `fill="currentColor"`.
|
||||
|
||||
The `role`/`aria-label` here are for standalone use; the card strips them and marks the
|
||||
inline SVG `aria-hidden="true" focusable="false"`, putting the accessible name on the
|
||||
link (`aria-label` + `title`, e.g. "GitHub profile").
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" role="img" aria-label="Fiverr"><path fill="currentColor" d="M23.004 15.588a.995.995 0 1 0 .002-1.99.995.995 0 0 0-.002 1.99zm-.996-3.705h-.85c-.546 0-.84.41-.84 1.092v2.466h-1.61v-3.558h-.684c-.547 0-.84.41-.84 1.092v2.466h-1.61v-4.874h1.61v.74c.264-.574.626-.74 1.163-.74h1.972v.74c.264-.574.625-.74 1.162-.74h.527v1.316zm-6.786 1.501h-3.359c.088.546.43.858 1.006.858.43 0 .732-.175.83-.487l1.425.4c-.351.848-1.22 1.364-2.255 1.364-1.748 0-2.549-1.355-2.549-2.515 0-1.14.703-2.505 2.45-2.505 1.856 0 2.471 1.384 2.471 2.408 0 .224-.01.37-.02.477zm-1.562-.945c-.04-.42-.342-.81-.889-.81-.508 0-.81.225-.908.81h1.797zM7.508 15.44h1.416l1.767-4.874h-1.62l-.86 2.837-.878-2.837H5.72l1.787 4.874zm-6.6 0H2.51v-3.558h1.524v3.558h1.591v-4.874H2.51v-.302c0-.332.235-.536.606-.536h.918V8.412H2.85c-1.162 0-1.943.712-1.943 1.755v.4H0v1.316h.908v3.558z"/></svg>
|
||||
|
After Width: | Height: | Size: 912 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" role="img" aria-label="GitHub"><path fill="currentColor" d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"/></svg>
|
||||
|
After Width: | Height: | Size: 842 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" role="img" aria-label="LinkedIn"><path fill="currentColor" d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433c-1.144 0-2.063-.926-2.063-2.065 0-1.138.92-2.063 2.063-2.063 1.14 0 2.064.925 2.064 2.063 0 1.139-.925 2.065-2.064 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z"/></svg>
|
||||
|
After Width: | Height: | Size: 630 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" role="img" aria-label="Upwork"><path fill="currentColor" d="M18.561 13.158c-1.102 0-2.135-.467-3.074-1.227l.228-1.076.008-.042c.207-1.143.849-3.06 2.839-3.06 1.492 0 2.703 1.212 2.703 2.703-.001 1.489-1.212 2.702-2.704 2.702zm0-8.14c-2.539 0-4.51 1.649-5.31 4.366-1.22-1.834-2.148-4.036-2.687-5.892H7.828v7.112c-.002 1.406-1.141 2.546-2.547 2.548-1.405-.002-2.543-1.143-2.545-2.548V3.492H0v7.112c0 2.914 2.37 5.303 5.281 5.303 2.913 0 5.283-2.389 5.283-5.303v-1.19c.529 1.107 1.182 2.229 1.974 3.221l-1.673 7.873h2.797l1.213-5.71c1.063.679 2.285 1.109 3.686 1.109 3 0 5.439-2.452 5.439-5.45 0-3-2.439-5.439-5.439-5.439z"/></svg>
|
||||
|
After Width: | Height: | Size: 689 B |
@@ -0,0 +1,141 @@
|
||||
/* The ONE source of truth for photo uploads — limits, validation and downscale.
|
||||
Two forms call this: the directory application form (/odoo-community-developers/)
|
||||
and the private self-service edit form (/freelancer-edit/). The limits live here
|
||||
once, so the two cannot drift. Loaded before each page's own script.
|
||||
|
||||
Why this exists beyond page weight: re-encoding to a canvas DROPS EXIF entirely.
|
||||
A phone photo carries GPS coordinates, so an un-stripped portrait publishes the
|
||||
photographer's home location. Re-encoding is also a security step — a file whose
|
||||
declared type was spoofed fails to decode and is refused here.
|
||||
|
||||
NOTE: the <input accept> attribute is a picker HINT, never validation. Only
|
||||
validate() below decides, and it runs on the File object before any read. */
|
||||
(function () {
|
||||
var MAX_INPUT_BYTES = 8 * 1024 * 1024; // 8 MB — refuse before reading a byte
|
||||
var ALLOWED_TYPES = ["image/jpeg", "image/png", "image/webp"];
|
||||
var MAX_SIDE = 384; // longest side of the stored image, px
|
||||
var MAX_OUTPUT_BYTES = 80 * 1024; // 80 KB — the encoded JPEG target
|
||||
var QUALITY = 0.82; // starting JPEG quality
|
||||
var QUALITY_STEP = 0.05; // step down until the target is met
|
||||
var QUALITY_FLOOR = 0.4; // never go below this
|
||||
|
||||
function kb(bytes) { return Math.round(bytes / 1024); }
|
||||
function mb(bytes) { return (bytes / (1024 * 1024)).toFixed(1); }
|
||||
|
||||
/* Decide on the File's own metadata — no read happens here. */
|
||||
function validate(file) {
|
||||
if (!file) return { ok: false, reason: "No file was chosen." };
|
||||
if (ALLOWED_TYPES.indexOf(file.type) === -1) {
|
||||
return { ok: false, reason: "That file type is not allowed. Use a JPEG, PNG or WebP image." };
|
||||
}
|
||||
if (file.size > MAX_INPUT_BYTES) {
|
||||
return { ok: false, reason: "That photo is " + mb(file.size) + " MB. The limit is 8 MB — please choose a smaller one." };
|
||||
}
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
function readAsDataUrl(file) {
|
||||
return new Promise(function (resolve, reject) {
|
||||
var reader = new FileReader();
|
||||
reader.onload = function () { resolve(reader.result); };
|
||||
reader.onerror = reject;
|
||||
reader.readAsDataURL(file);
|
||||
});
|
||||
}
|
||||
|
||||
function loadImage(url) {
|
||||
return new Promise(function (resolve, reject) {
|
||||
var img = new Image();
|
||||
img.onload = function () { resolve(img); };
|
||||
img.onerror = reject; // a spoofed "image/jpeg" that is not an image lands here
|
||||
img.src = url;
|
||||
});
|
||||
}
|
||||
|
||||
function byteLength(dataUrl) {
|
||||
return atob(dataUrl.slice(dataUrl.indexOf(",") + 1)).length;
|
||||
}
|
||||
|
||||
/* Read → centre-crop square → cap longest side → encode JPEG → step quality down
|
||||
until it fits. Resolves { dataUrl, width, height, bytes, quality }. */
|
||||
function downscale(file) {
|
||||
return readAsDataUrl(file).then(loadImage).then(function (img) {
|
||||
var w = img.naturalWidth, h = img.naturalHeight;
|
||||
var side = Math.min(w, h);
|
||||
var sx = (w - side) / 2, sy = (h - side) / 2;
|
||||
var out = Math.min(MAX_SIDE, side);
|
||||
var canvas = document.createElement("canvas");
|
||||
canvas.width = out;
|
||||
canvas.height = out;
|
||||
canvas.getContext("2d").drawImage(img, sx, sy, side, side, 0, 0, out, out);
|
||||
var quality = QUALITY;
|
||||
var dataUrl = canvas.toDataURL("image/jpeg", quality);
|
||||
var bytes = byteLength(dataUrl);
|
||||
while (bytes > MAX_OUTPUT_BYTES && quality > QUALITY_FLOOR) {
|
||||
quality = Math.max(QUALITY_FLOOR, quality - QUALITY_STEP);
|
||||
dataUrl = canvas.toDataURL("image/jpeg", quality);
|
||||
bytes = byteLength(dataUrl);
|
||||
}
|
||||
return { dataUrl: dataUrl, width: out, height: out, bytes: bytes, quality: quality };
|
||||
});
|
||||
}
|
||||
|
||||
/* The one honest line the person sees. No "optimised for the web" — only what we can show. */
|
||||
function resultLine(res) {
|
||||
return "Resized to " + res.width + " × " + res.height + ", " + kb(res.bytes) + " KB";
|
||||
}
|
||||
|
||||
/* Wire one file input to a preview <img> and a status line. onResult receives the
|
||||
downscaled data URL, or null when there is no (valid) file. Both forms use this,
|
||||
so the outcome shown is identical. */
|
||||
function wire(input, preview, result, onResult) {
|
||||
if (!input) return;
|
||||
function say(text, state) {
|
||||
if (!result) return;
|
||||
result.textContent = text || "";
|
||||
if (state) result.setAttribute("data-state", state);
|
||||
else result.removeAttribute("data-state");
|
||||
}
|
||||
input.addEventListener("change", function () {
|
||||
var file = input.files && input.files[0];
|
||||
if (!file) {
|
||||
if (preview) preview.hidden = true;
|
||||
say("", null);
|
||||
onResult(null);
|
||||
return;
|
||||
}
|
||||
var v = validate(file);
|
||||
if (!v.ok) {
|
||||
if (preview) preview.hidden = true;
|
||||
input.value = ""; // a refused file must never be submitted
|
||||
say(v.reason, "err");
|
||||
onResult(null);
|
||||
return;
|
||||
}
|
||||
say("Processing…", null);
|
||||
downscale(file).then(function (res) {
|
||||
if (preview) { preview.src = res.dataUrl; preview.hidden = false; }
|
||||
say(resultLine(res), "ok");
|
||||
onResult(res.dataUrl, res);
|
||||
}).catch(function () {
|
||||
if (preview) preview.hidden = true;
|
||||
input.value = "";
|
||||
say("That file could not be read as an image. Please choose a JPEG, PNG or WebP.", "err");
|
||||
onResult(null);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
window.O4PPhoto = {
|
||||
MAX_INPUT_BYTES: MAX_INPUT_BYTES,
|
||||
ALLOWED_TYPES: ALLOWED_TYPES,
|
||||
MAX_SIDE: MAX_SIDE,
|
||||
MAX_OUTPUT_BYTES: MAX_OUTPUT_BYTES,
|
||||
QUALITY: QUALITY,
|
||||
validate: validate,
|
||||
downscale: downscale,
|
||||
resultLine: resultLine,
|
||||
kb: kb,
|
||||
wire: wire
|
||||
};
|
||||
})();
|
||||
@@ -6,6 +6,19 @@
|
||||
========================================================= */
|
||||
|
||||
* { box-sizing: border-box; }
|
||||
|
||||
/* `hidden` must really hide — everywhere, not one element at a time. The UA's own
|
||||
`[hidden] { display: none }` is the weakest possible declaration: any page rule that
|
||||
sets `display` on the same element outranks it. A class rule (`.dir-pager`,
|
||||
`.dev-card`, `.hero-notice`) already beats it, and an id-based state selector such as
|
||||
`.js #directory[data-dir-state="ready"] #dirGrid > .dev-card` (two ids) beat the old
|
||||
per-element patch — so setting `el.hidden = true` set the attribute and the element
|
||||
stayed on screen (measured on /odoo-community-developers/: four cards visible, the
|
||||
count line reading "Showing 1 of 4"). This one rule, with `!important`, beats every
|
||||
class- or id-based `display`, so the attribute hides its element no matter which
|
||||
later rule sets `display`. It is the single mechanism for `[hidden]` site-wide; the
|
||||
per-element `[hidden]` rules are gone so nothing disagrees. */
|
||||
[hidden] { display: none !important; }
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: Inter, 'Segoe UI', sans-serif;
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
rel="stylesheet"
|
||||
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css"
|
||||
/>
|
||||
<link rel="stylesheet" href="style.css" />
|
||||
<link rel="stylesheet" href="style.css?v=061517baef" />
|
||||
<style>
|
||||
/* Language selector — the page's top row: the back-link stays left, the selector travels right.
|
||||
Cream canvas, so lang.js's own dark-brass active colour is the correct one (no override). */
|
||||
@@ -274,9 +274,10 @@
|
||||
window.plausible=window.plausible||function(){(plausible.q=plausible.q||[]).push(arguments)},plausible.init=plausible.init||function(i){plausible.o=i||{}};
|
||||
plausible.init()
|
||||
</script>
|
||||
<script defer src="/static/lang.js"></script>
|
||||
<script defer src="/static/lang.js?v=3ec49a7e62"></script>
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<div class="container">
|
||||
<div class="blog-header">
|
||||
<a href="index.html">← Back to Home</a>
|
||||
@@ -292,6 +293,8 @@
|
||||
For the fastest help, please read our <strong>FAQ</strong> first — our support
|
||||
bot can assist with quick fixes like <strong>git pull</strong> or restarts — or
|
||||
simply send a <strong>ticket</strong> to our support team for further assistance.
|
||||
Urgent topics can be handled by the <strong>AI agent</strong> we offer, and email
|
||||
support answers within <strong>24 hours</strong>.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
@@ -586,6 +589,7 @@
|
||||
});
|
||||
</script>
|
||||
|
||||
<script defer src="/static/agent-odoo4projects/agent.js"></script>
|
||||
</main>
|
||||
<script defer src="/static/agent-odoo4projects/agent.js?v=c3bb0cee2b"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -25,7 +25,7 @@
|
||||
rel="stylesheet"
|
||||
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css"
|
||||
/>
|
||||
<link rel="stylesheet" href="style.css" />
|
||||
<link rel="stylesheet" href="style.css?v=061517baef" />
|
||||
<style>
|
||||
.legal-hero {
|
||||
padding: 4rem 0 2.5rem;
|
||||
@@ -111,6 +111,7 @@
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<div class="container">
|
||||
<div class="blog-header">
|
||||
<a href="index.html">← Back to Home</a>
|
||||
@@ -130,7 +131,7 @@
|
||||
<div class="container">
|
||||
<div class="legal-card">
|
||||
<h2>Who we are</h2>
|
||||
<p>ODOO4projects LLC, 30 N Gould St, STE R, Sheridan, WY 82801, USA. Contact: <span class="contact-email">support@ODOO4projects.com</span></p>
|
||||
<p>ODOO4projects LLC, 30 N Gould St, STE R, Sheridan, WY 82801, USA. Contact: <span class="contact-email">support@odoo4projects.com</span></p>
|
||||
<p>These terms cover the hosting service we sell on this website.</p>
|
||||
</div>
|
||||
|
||||
@@ -153,17 +154,17 @@
|
||||
|
||||
<div class="legal-card">
|
||||
<h2>Price and payment</h2>
|
||||
<p>Plans are billed yearly, in advance, in US dollars, plus tax where it applies. Payment is taken by card through Stripe. The current prices are the ones on the pricing page.</p>
|
||||
<p>Plans are billed yearly, in advance, in US dollars, tax included — the price you see is the price you pay. Payment is taken by card through Stripe. The current prices are the ones on the pricing page.</p>
|
||||
</div>
|
||||
|
||||
<div class="legal-card">
|
||||
<h2>Cancelling</h2>
|
||||
<p>You can cancel at any time. Cancelling stops the next renewal. It does not refund the period you have already paid for. We make no refund promise beyond what is written here.</p>
|
||||
<p>You can cancel at any time. Cancelling stops the next renewal, and we refund the time you have not used — the part of the period you have paid for that is still ahead of you, pro rata against the period you paid for.</p>
|
||||
</div>
|
||||
|
||||
<div class="legal-card">
|
||||
<h2>Version upgrades and migrations</h2>
|
||||
<p>Ask our support team for a version upgrade. We migrate databases on the main Odoo releases 17, 18 and 19. We do not support patch releases such as 18.1.</p>
|
||||
<p>Updates and upgrades within your current Odoo version are included in your plan. Moving a database to a new major Odoo version is a migration: it is a separate service and is quoted separately, so ask our support team for a quote. Our migration service covers databases on the main Odoo releases 17, 18 and 19. New instances can run Odoo 20, the current release. We do not support patch releases such as 18.1.</p>
|
||||
</div>
|
||||
|
||||
<div class="legal-card">
|
||||
@@ -193,6 +194,7 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<script defer src="/static/agent-odoo4projects/agent.js"></script>
|
||||
</main>
|
||||
<script defer src="/static/agent-odoo4projects/agent.js?v=c3bb0cee2b"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -27,8 +27,7 @@
|
||||
onload="this.media='all'"
|
||||
/>
|
||||
<noscript><link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Marcellus&display=swap" /></noscript>
|
||||
<link rel="stylesheet" href="/style.css" media="print" onload="this.media='all'" />
|
||||
<noscript><link rel="stylesheet" href="/style.css" /></noscript>
|
||||
<link rel="stylesheet" href="/style.css?v=061517baef" />
|
||||
<style>
|
||||
/* ---- "What Can I Use Odoo For?" guide page (page-scoped; additive).
|
||||
The block below is odoo-community-upgrade.html's page-scoped CSS, reused so
|
||||
@@ -518,6 +517,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Hero: the question, answered in the first hundred words -->
|
||||
<main>
|
||||
<section class="hero oc-hero">
|
||||
<div class="hero-bg">
|
||||
<img
|
||||
@@ -787,7 +787,7 @@
|
||||
<p>
|
||||
<strong>If you would rather learn it by doing it,</strong> the
|
||||
<a href="/odoo-erp-automation-bootcamp/">ODOO ERP Automation Bootcamp</a> is a ten-session live
|
||||
course at USD 465.00 with a year of hosted Odoo Community included, and the two-hour
|
||||
course at USD 595.00 with a year of hosted Odoo Community included, and the two-hour
|
||||
<a href="/odoo-n8n-masterclass/">ODOO x N8N Masterclass</a> is USD 120.00.
|
||||
</p>
|
||||
</div>
|
||||
@@ -904,6 +904,7 @@
|
||||
|
||||
<!-- Footer (copied from odoo-community-upgrade.html; icon-font glyphs replaced with inline SVG so the page
|
||||
carries no external icon dependency) -->
|
||||
</main>
|
||||
<footer class="footer">
|
||||
<div class="container">
|
||||
<div class="fgrid">
|
||||
@@ -912,6 +913,7 @@
|
||||
<ul>
|
||||
<li><a href="/odoo-erp-automation-bootcamp/">ODOO Bootcamp</a></li>
|
||||
<li><a href="/odoo-hosting-2027.html">ODOO Hosting in 2027</a></li>
|
||||
<li><a href="/odoo-community-developers/">Odoo Freelancer Directory</a></li>
|
||||
<li><a href="/support.html">Support</a></li>
|
||||
<li><a href="/legal.html">Legal</a></li>
|
||||
</ul>
|
||||
@@ -935,7 +937,7 @@
|
||||
<a href="https://www.linkedin.com/company/odoo4projects/" target="_blank" aria-label="LinkedIn"><svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M4.98 3.5a2.5 2.5 0 1 1 0 5 2.5 2.5 0 0 1 0-5zM3 9h4v12H3zM9 9h3.8v1.7h.05c.53-.95 1.83-1.95 3.76-1.95 4.02 0 4.76 2.5 4.76 5.75V21h-4v-5.5c0-1.31-.02-3-1.9-3-1.9 0-2.19 1.42-2.19 2.9V21H9z" /></svg></a>
|
||||
<a href="https://www.youtube.com/@ODOO4projects" target="_blank" aria-label="YouTube"><svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M23 12s0-3.2-.41-4.73a2.5 2.5 0 0 0-1.76-1.77C19.3 5.1 12 5.1 12 5.1s-7.3 0-8.83.4A2.5 2.5 0 0 0 1.4 7.27C1 8.8 1 12 1 12s0 3.2.41 4.73a2.5 2.5 0 0 0 1.76 1.77c1.53.4 8.83.4 8.83.4s7.3 0 8.83-.4a2.5 2.5 0 0 0 1.76-1.77C23 15.2 23 12 23 12zM9.75 15.02V8.98L15.5 12z" /></svg></a>
|
||||
</div>
|
||||
<form class="footer-contact-form" id="footerContactForm" action="mailto:hello@odoo4projects.com" method="post" enctype="text/plain">
|
||||
<form class="footer-contact-form" id="footerContactForm" action="mailto:support@odoo4projects.com" method="post" enctype="text/plain">
|
||||
<label class="oc-sr-only" for="footerContactEmail">Your email address</label>
|
||||
<input type="email" id="footerContactEmail" name="email" placeholder="Your email address" required />
|
||||
<div class="footer-textarea-wrap">
|
||||
@@ -950,6 +952,6 @@
|
||||
<div class="fcopy">Copyright © ODOO4projects LLC</div>
|
||||
</div>
|
||||
</footer>
|
||||
<script defer src="/static/lang.js"></script>
|
||||
<script defer src="/static/lang.js?v=3ec49a7e62"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -6,13 +6,13 @@
|
||||
<title>ODOO Workshops and Classes: Live Courses from USD 120</title>
|
||||
<meta
|
||||
name="description"
|
||||
content="Two live courses with Oliver Arnold: the two-hour ODOO x N8N Masterclass at USD 120.00 and the ten-session ODOO ERP Automation Bootcamp at USD 465.00."
|
||||
content="Two live courses with Oliver Arnold: the two-hour ODOO x N8N Masterclass at USD 120.00 and the ten-session ODOO ERP Automation Bootcamp at USD 595.00."
|
||||
/>
|
||||
<link rel="canonical" href="https://odoo4projects.com/workshops/" />
|
||||
<meta property="og:title" content="ODOO Workshops and Classes: Live Courses from USD 120" />
|
||||
<meta
|
||||
property="og:description"
|
||||
content="Two live courses with Oliver Arnold: the two-hour ODOO x N8N Masterclass at USD 120.00 and the ten-session ODOO ERP Automation Bootcamp at USD 465.00."
|
||||
content="Two live courses with Oliver Arnold: the two-hour ODOO x N8N Masterclass at USD 120.00 and the ten-session ODOO ERP Automation Bootcamp at USD 595.00."
|
||||
/>
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:url" content="https://odoo4projects.com/workshops/" />
|
||||
@@ -27,8 +27,7 @@
|
||||
onload="this.media='all'"
|
||||
/>
|
||||
<noscript><link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Marcellus&display=swap" /></noscript>
|
||||
<link rel="stylesheet" href="/style.css" media="print" onload="this.media='all'" />
|
||||
<noscript><link rel="stylesheet" href="/style.css" /></noscript>
|
||||
<link rel="stylesheet" href="/style.css?v=061517baef" />
|
||||
<style>
|
||||
/* ---- Workshops and Classes hub page (page-scoped; additive).
|
||||
The block below is the bootcamp page's page-scoped CSS, reused so the two
|
||||
@@ -477,7 +476,7 @@
|
||||
"provider": { "@type": "Organization", "name": "ODOO4projects", "url": "https://odoo4projects.com/" },
|
||||
"instructor": { "@type": "Person", "name": "Oliver Arnold", "jobTitle": "Technical Lead" },
|
||||
"inLanguage": "en",
|
||||
"offers": { "@type": "Offer", "price": "465.00", "priceCurrency": "USD", "category": "Paid" },
|
||||
"offers": { "@type": "Offer", "price": "595.00", "priceCurrency": "USD", "category": "Paid" },
|
||||
"hasCourseInstance": { "@type": "CourseInstance", "courseMode": "online", "courseWorkload": "PT10H" }
|
||||
}
|
||||
}
|
||||
@@ -506,6 +505,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Hero -->
|
||||
<main>
|
||||
<section class="hero bc-hero">
|
||||
<div class="container">
|
||||
<p class="hero-kicker">TWO LIVE COURSES · ONLINE · OLIVER ARNOLD · ENGLISH</p>
|
||||
@@ -551,7 +551,7 @@
|
||||
</li>
|
||||
<li>
|
||||
<h3>ODOO ERP Automation Bootcamp</h3>
|
||||
<p class="bc-card-price">USD 465.00 · 10 live sessions, 1 hour each</p>
|
||||
<p class="bc-card-price">USD 595.00 · 10 live sessions, 1 hour each</p>
|
||||
<p>You leave with a working ERP for your own company: your products, your customers, your quotes, your invoices and your stock in one system you can keep running yourself.</p>
|
||||
<p class="bc-card-format">Live and online, every Thursday 18:00–19:00 Central Time · One year of hosted Odoo Community Edition included · Limited to 6 participants · English</p>
|
||||
<p class="bc-card-cta"><a class="btn btn-brass" href="/odoo-erp-automation-bootcamp/">More Info</a></p>
|
||||
@@ -700,7 +700,7 @@
|
||||
<tr>
|
||||
<th scope="row">Price</th>
|
||||
<td>USD 120.00</td>
|
||||
<td>USD 465.00</td>
|
||||
<td>USD 595.00</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Length</th>
|
||||
@@ -740,6 +740,7 @@
|
||||
|
||||
<!-- Footer (copied from index.html; icon-font glyphs replaced with inline SVG so the page
|
||||
carries no external icon dependency) -->
|
||||
</main>
|
||||
<footer class="footer">
|
||||
<div class="container">
|
||||
<div class="fgrid">
|
||||
@@ -748,6 +749,7 @@
|
||||
<ul>
|
||||
<li><a href="/odoo-erp-automation-bootcamp/">ODOO Bootcamp</a></li>
|
||||
<li><a href="/odoo-hosting-2027.html">ODOO Hosting in 2027</a></li>
|
||||
<li><a href="/odoo-community-developers/">Odoo Freelancer Directory</a></li>
|
||||
<li><a href="/support.html">Support</a></li>
|
||||
<li><a href="/legal.html">Legal</a></li>
|
||||
</ul>
|
||||
@@ -771,7 +773,7 @@
|
||||
<a href="https://www.linkedin.com/company/odoo4projects/" target="_blank" aria-label="LinkedIn"><svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M4.98 3.5a2.5 2.5 0 1 1 0 5 2.5 2.5 0 0 1 0-5zM3 9h4v12H3zM9 9h3.8v1.7h.05c.53-.95 1.83-1.95 3.76-1.95 4.02 0 4.76 2.5 4.76 5.75V21h-4v-5.5c0-1.31-.02-3-1.9-3-1.9 0-2.19 1.42-2.19 2.9V21H9z" /></svg></a>
|
||||
<a href="https://www.youtube.com/@ODOO4projects" target="_blank" aria-label="YouTube"><svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M23 12s0-3.2-.41-4.73a2.5 2.5 0 0 0-1.76-1.77C19.3 5.1 12 5.1 12 5.1s-7.3 0-8.83.4A2.5 2.5 0 0 0 1.4 7.27C1 8.8 1 12 1 12s0 3.2.41 4.73a2.5 2.5 0 0 0 1.76 1.77c1.53.4 8.83.4 8.83.4s7.3 0 8.83-.4a2.5 2.5 0 0 0 1.76-1.77C23 15.2 23 12 23 12zM9.75 15.02V8.98L15.5 12z" /></svg></a>
|
||||
</div>
|
||||
<form class="footer-contact-form" id="footerContactForm" action="mailto:hello@odoo4projects.com" method="post" enctype="text/plain">
|
||||
<form class="footer-contact-form" id="footerContactForm" action="mailto:support@odoo4projects.com" method="post" enctype="text/plain">
|
||||
<label class="bc-sr-only" for="footerContactEmail">Your email address</label>
|
||||
<input type="email" id="footerContactEmail" name="email" placeholder="Your email address" required />
|
||||
<div class="footer-textarea-wrap">
|
||||
@@ -786,6 +788,6 @@
|
||||
<div class="fcopy">Copyright © ODOO4projects LLC</div>
|
||||
</div>
|
||||
</footer>
|
||||
<script defer src="/static/lang.js"></script>
|
||||
<script defer src="/static/lang.js?v=3ec49a7e62"></script>
|
||||
</body>
|
||||
</html>
|
||||