- Contact card in hero emailing inquiries via SMTP (secrets in .env) - Three-step How it works section - Point CTAs at the contact form - Local site port 8082 Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>Contact
| @@ -0,0 +1,2 @@ | |||
| # Copy this file to .env and fill in the real values. .env is never committed. | |||
| SMTP_PASSWORD=your-mailbox-password | |||
| @@ -7,3 +7,6 @@ | |||
| *.log | |||
| *.user | |||
| *.suo | |||
| # Secrets | |||
| /.env | |||
| @@ -6,7 +6,7 @@ A clean Classic ASP application scaffold based on the RouteKit MVC framework. | |||
| 1. Install IIS Express with Classic ASP enabled. | |||
| 2. Run `run_site.cmd`. | |||
| 3. Browse to `http://localhost:8080/`. | |||
| 3. Browse to `http://localhost:8082/`. | |||
| The landing page is served by `HomeController.Index` through | |||
| `app/views/Home/landing.asp`. The earlier logo-reference page remains at | |||
| @@ -3,9 +3,21 @@ Class HomeController_Class | |||
| Public Title | |||
| Public useLayout | |||
| ' Contact form state shared with the landing view | |||
| Private contactErrors | |||
| Private contactName | |||
| Private contactEmail | |||
| Private contactPhone | |||
| Private contactBusiness | |||
| Private contactMessage | |||
| Private contactSent | |||
| Private Sub Class_Initialize() | |||
| Title = "PostcardMojo Logo Files" | |||
| useLayout = False | |||
| contactErrors = Array() | |||
| ReDim contactErrors(-1) | |||
| contactSent = (Request.QueryString("contact") = "sent") | |||
| End Sub | |||
| Public Sub Index() | |||
| @@ -13,6 +25,42 @@ Class HomeController_Class | |||
| <!--#include file="../views/Home/landing.asp" --> | |||
| <% | |||
| End Sub | |||
| ' GetAntiCSRFToken returns an empty value the first time it creates a token, so read it again. | |||
| Private Function ContactCsrfToken() | |||
| ContactCsrfToken = HTMLSecurity().GetAntiCSRFToken("home.contact") | |||
| If Len(ContactCsrfToken) = 0 Then ContactCsrfToken = HTMLSecurity().GetAntiCSRFToken("home.contact") | |||
| End Function | |||
| ' POST /contact - validate the inquiry and email the PostcardMojo team. | |||
| Public Sub Contact() | |||
| contactName = Trim(Request.Form("name")) | |||
| contactEmail = Trim(Request.Form("email")) | |||
| contactPhone = Trim(Request.Form("phone")) | |||
| contactBusiness = Trim(Request.Form("business")) | |||
| contactMessage = Trim(Request.Form("message")) | |||
| ' Honeypot: real visitors never see or fill this field | |||
| If Len(Request.Form("zq_trap_field")) > 0 Then | |||
| Response.Redirect "/?contact=sent#contact" | |||
| End If | |||
| If Not HTMLSecurity().IsValidAntiCSRFToken("home.contact", Request.Form("csrf_token")) Then | |||
| contactErrors = Array("Your session expired. Please try again.") | |||
| Else | |||
| contactErrors = ContactInquiryService().ValidationErrors(contactName, contactEmail, contactBusiness, contactMessage) | |||
| End If | |||
| If UBound(contactErrors) < 0 Then | |||
| If ContactInquiryService().Notify(contactName, contactEmail, contactPhone, contactBusiness, contactMessage) Then | |||
| HTMLSecurity().ClearAntiCSRFToken "home.contact" | |||
| Response.Redirect "/?contact=sent#contact" | |||
| End If | |||
| contactErrors = Array("We could not send your message. Please email danielcovington@postcardmojo.com directly.") | |||
| End If | |||
| Index | |||
| End Sub | |||
| End Class | |||
| Dim HomeController_Class__Singleton | |||
| @@ -1 +1,3 @@ | |||
| <!--#include file="../services/EnvService.asp" --> | |||
| <!--#include file="../services/ContactInquiryService.asp" --> | |||
| <!--#include file="HomeController.asp" --> | |||
| @@ -0,0 +1,81 @@ | |||
| <% | |||
| ' Validates a visitor inquiry and notifies the PostcardMojo team by email. | |||
| Class ContactInquiryService_Class | |||
| Public Function ValidationErrors(name, email, business, message) | |||
| Dim errs : errs = Array() | |||
| ReDim errs(-1) | |||
| If Len(name) = 0 Then AddMessage errs, "Please enter your name." | |||
| If Len(name) > 100 Then AddMessage errs, "Name must be 100 characters or fewer." | |||
| If Len(email) = 0 Then | |||
| AddMessage errs, "Please enter your email address." | |||
| ElseIf Len(email) > 150 Or Not IsValidEmail(email) Then | |||
| AddMessage errs, "Please enter a valid email address." | |||
| End If | |||
| If Len(business) > 150 Then AddMessage errs, "Business name must be 150 characters or fewer." | |||
| If Len(message) > 2000 Then AddMessage errs, "Message must be 2000 characters or fewer." | |||
| ValidationErrors = errs | |||
| End Function | |||
| Public Function Notify(name, email, phone, business, message) | |||
| Dim recipient, smtpServer, smtpPort, smtpUser, body | |||
| recipient = Setting("ContactRecipient", "covington.daniel@gmail.com") | |||
| smtpServer = Setting("SmtpServer", "localhost") | |||
| smtpPort = Setting("SmtpPort", "25") | |||
| smtpUser = Setting("SmtpUsername", "") | |||
| body = "New PostcardMojo website inquiry" & vbCrLf & vbCrLf & _ | |||
| "Name: " & name & vbCrLf & _ | |||
| "Email: " & email & vbCrLf & _ | |||
| "Phone: " & phone & vbCrLf & _ | |||
| "Business: " & business & vbCrLf & vbCrLf & _ | |||
| "Message:" & vbCrLf & message & vbCrLf & vbCrLf & _ | |||
| "Received: " & Now() & vbCrLf & _ | |||
| "IP: " & Request.ServerVariables("REMOTE_ADDR") | |||
| CDOEmail.From = Setting("SmtpFrom", Choice(Len(smtpUser) > 0, smtpUser, recipient)) | |||
| CDOEmail.Subject = "PostcardMojo inquiry from " & StripLineBreaks(name) | |||
| CDOEmail.Body = body | |||
| CDOEmail.IsBodyHTML = False | |||
| CDOEmail.SMTPServer = smtpServer | |||
| CDOEmail.SMTPPort = CLng(smtpPort) | |||
| CDOEmail.SMTPUsername = smtpUser | |||
| CDOEmail.SMTPPassword = EnvService().Value("SMTP_PASSWORD", "") | |||
| CDOEmail.SMTPUseSSL = (LCase(Setting("SmtpUseSsl", "false")) = "true") | |||
| CDOEmail.AddRecipient "TO", recipient | |||
| Notify = CDOEmail.Send() | |||
| End Function | |||
| Public Function StripLineBreaks(value) | |||
| StripLineBreaks = Replace(Replace(value, vbCr, " "), vbLf, " ") | |||
| End Function | |||
| Private Function Setting(key, fallback) | |||
| Dim value : value = GetAppSetting(key) | |||
| If value = "nothing" Or Len(value) = 0 Then value = fallback | |||
| Setting = value | |||
| End Function | |||
| Private Function IsValidEmail(value) | |||
| Dim re : Set re = New RegExp | |||
| re.Pattern = "^[^\s@<>,;""]+@[^\s@<>,;""]+\.[A-Za-z]{2,}$" | |||
| IsValidEmail = re.Test(value) | |||
| End Function | |||
| Private Sub AddMessage(ByRef errs, msg) | |||
| ReDim Preserve errs(UBound(errs) + 1) | |||
| errs(UBound(errs)) = msg | |||
| End Sub | |||
| End Class | |||
| Dim ContactInquiryService_Class__Singleton | |||
| Function ContactInquiryService() | |||
| If IsEmpty(ContactInquiryService_Class__Singleton) Then | |||
| Set ContactInquiryService_Class__Singleton = New ContactInquiryService_Class | |||
| End If | |||
| Set ContactInquiryService = ContactInquiryService_Class__Singleton | |||
| End Function | |||
| %> | |||
| @@ -0,0 +1,56 @@ | |||
| <% | |||
| ' Reads KEY=VALUE secrets from the .env file in the project root (outside the | |||
| ' public web root). Real environment variables take precedence over the file. | |||
| Class EnvService_Class | |||
| Private values | |||
| Private Sub Class_Initialize() | |||
| Set values = Server.CreateObject("Scripting.Dictionary") | |||
| values.CompareMode = 1 | |||
| LoadFile | |||
| End Sub | |||
| Public Function Value(key, fallback) | |||
| Dim shell : Set shell = Server.CreateObject("WScript.Shell") | |||
| Value = shell.Environment("Process")(key) | |||
| Set shell = Nothing | |||
| If Len(Value) > 0 Then Exit Function | |||
| If values.Exists(key) Then | |||
| Value = values(key) | |||
| Else | |||
| Value = fallback | |||
| End If | |||
| End Function | |||
| Private Sub LoadFile() | |||
| Dim fso, path, stream, line, pos, key, val | |||
| Set fso = Server.CreateObject("Scripting.FileSystemObject") | |||
| path = Server.MapPath("..\.env") | |||
| If Not fso.FileExists(path) Then Exit Sub | |||
| Set stream = fso.OpenTextFile(path, 1, False) | |||
| Do Until stream.AtEndOfStream | |||
| line = Trim(stream.ReadLine()) | |||
| If Len(line) > 0 And Left(line, 1) <> "#" Then | |||
| pos = InStr(line, "=") | |||
| If pos > 1 Then | |||
| key = Trim(Left(line, pos - 1)) | |||
| val = Trim(Mid(line, pos + 1)) | |||
| If Len(val) > 1 Then | |||
| If (Left(val, 1) = """" And Right(val, 1) = """") Or (Left(val, 1) = "'" And Right(val, 1) = "'") Then val = Mid(val, 2, Len(val) - 2) | |||
| End If | |||
| values(key) = val | |||
| End If | |||
| End If | |||
| Loop | |||
| stream.Close | |||
| End Sub | |||
| End Class | |||
| Dim EnvService_Class__Singleton | |||
| Function EnvService() | |||
| If IsEmpty(EnvService_Class__Singleton) Then | |||
| Set EnvService_Class__Singleton = New EnvService_Class | |||
| End If | |||
| Set EnvService = EnvService_Class__Singleton | |||
| End Function | |||
| %> | |||
| @@ -30,7 +30,7 @@ Response.CodePage = 65001 | |||
| <a href="#good-fit">Who it is for</a> | |||
| <a href="#faq">FAQ</a> | |||
| </nav> | |||
| <a class="button button-small button-primary" href="#get-started">Find your spot</a> | |||
| <a class="button button-small button-primary" href="#contact">Find your spot</a> | |||
| </div> | |||
| </header> | |||
| @@ -42,9 +42,9 @@ Response.CodePage = 65001 | |||
| <div class="hero-copy"> | |||
| <p class="eyebrow"><span></span> Shared direct mail for local businesses</p> | |||
| <h1>Make your business <em>impossible to miss</em> in the mailbox.</h1> | |||
| <p class="hero-lede">PostcardMojo puts a curated lineup of local businesses on one oversized 9×12 postcard. Your offer gets a bold, tangible place in nearby homes while participating businesses share the cost of printing and postage.</p> | |||
| <p class="hero-lede">PostcardMojo puts a curated lineup of local businesses on one oversized 9×12 postcard and is delivered to 5,000 mailboxes. Your offer gets a bold, tangible place in nearby homes while participating businesses share the cost of printing and postage.</p> | |||
| <div class="hero-actions"> | |||
| <a class="button button-primary" href="#get-started">Explore a campaign</a> | |||
| <a class="button button-primary" href="#contact">Explore a campaign</a> | |||
| <a class="button button-quiet" href="#how-it-works">See how it works <span aria-hidden="true">→</span></a> | |||
| </div> | |||
| <ul class="hero-points" aria-label="Campaign highlights"> | |||
| @@ -61,6 +61,44 @@ Response.CodePage = 65001 | |||
| </div> | |||
| <div class="stage-note"><span aria-hidden="true">↗</span> One card. Multiple businesses. A whole neighborhood of opportunity.</div> | |||
| </div> | |||
| <aside class="contact-card" id="contact" aria-labelledby="contact-title"> | |||
| <p class="eyebrow eyebrow-on-dark"><span></span> Contact us</p> | |||
| <h2 id="contact-title">Interested in a spot on the next card?</h2> | |||
| <% If contactSent Then %> | |||
| <p class="contact-success" role="status">Thank you! Your message was sent and we will be in touch soon.</p> | |||
| <% Else %> | |||
| <form class="contact-form" method="post" action="/contact#contact" novalidate> | |||
| <% If UBound(contactErrors) >= 0 Then %> | |||
| <div class="contact-errors" role="alert"> | |||
| <ul> | |||
| <% Dim contactErrorIndex : For contactErrorIndex = 0 To UBound(contactErrors) %> | |||
| <li><%= Server.HTMLEncode(contactErrors(contactErrorIndex)) %></li> | |||
| <% Next %> | |||
| </ul> | |||
| </div> | |||
| <% End If %> | |||
| <input type="hidden" name="csrf_token" value="<%= Server.HTMLEncode(ContactCsrfToken()) %>"> | |||
| <div class="contact-trap" aria-hidden="true"><label>Leave this empty <input type="text" name="zq_trap_field" tabindex="-1" autocomplete="off"></label></div> | |||
| <label>Name <span aria-hidden="true">*</span> | |||
| <input type="text" name="name" maxlength="100" required autocomplete="name" value="<%= Server.HTMLEncode(contactName & "") %>"> | |||
| </label> | |||
| <label>Email <span aria-hidden="true">*</span> | |||
| <input type="email" name="email" maxlength="150" required autocomplete="email" value="<%= Server.HTMLEncode(contactEmail & "") %>"> | |||
| </label> | |||
| <label>Phone | |||
| <input type="tel" name="phone" maxlength="40" autocomplete="tel" value="<%= Server.HTMLEncode(contactPhone & "") %>"> | |||
| </label> | |||
| <label>Business | |||
| <input type="text" name="business" maxlength="150" autocomplete="organization" value="<%= Server.HTMLEncode(contactBusiness & "") %>"> | |||
| </label> | |||
| <label class="contact-wide">How can we help? | |||
| <textarea name="message" rows="3" maxlength="2000"><%= Server.HTMLEncode(contactMessage & "") %></textarea> | |||
| </label> | |||
| <button class="button button-primary" type="submit">Send my interest</button> | |||
| </form> | |||
| <% End If %> | |||
| </aside> | |||
| </div> | |||
| </section> | |||
| @@ -77,33 +115,27 @@ Response.CodePage = 65001 | |||
| <div class="shell"> | |||
| <div class="section-heading heading-centered"> | |||
| <p class="eyebrow eyebrow-centered"><span></span> Simple by design</p> | |||
| <h2>From your offer to their mailbox.</h2> | |||
| <p>PostcardMojo turns a group of complementary local businesses into one well-organized, high-visibility mail piece.</p> | |||
| <h2>Three steps to the mailbox.</h2> | |||
| <p>Pick your size, give us your ad, and we handle the printing and mailing.</p> | |||
| </div> | |||
| <ol class="steps"> | |||
| <li> | |||
| <span class="step-number">01</span> | |||
| <div class="step-icon" aria-hidden="true">◎</div> | |||
| <h3>Choose your campaign</h3> | |||
| <p>Start with the local area and timing that fit your business goals.</p> | |||
| <h3>Choose your ad size</h3> | |||
| <p>Pick a 3×4 or a 6×8 space on the shared 9×12 postcard.</p> | |||
| </li> | |||
| <li> | |||
| <span class="step-number">02</span> | |||
| <div class="step-icon" aria-hidden="true">◇</div> | |||
| <h3>Claim your category</h3> | |||
| <p>Reserve a space for your type of business, subject to campaign availability.</p> | |||
| </li> | |||
| <li> | |||
| <span class="step-number">03</span> | |||
| <div class="step-icon" aria-hidden="true">✦</div> | |||
| <h3>Shape the offer</h3> | |||
| <p>Bring your logo, message, and promotion together in a clear, focused ad.</p> | |||
| <h3>Send your layout or let us design it</h3> | |||
| <p>Already have an ad? Send it over. Need one? We will design it for you.</p> | |||
| </li> | |||
| <li> | |||
| <span class="step-number">04</span> | |||
| <span class="step-number">03</span> | |||
| <div class="step-icon" aria-hidden="true">↗</div> | |||
| <h3>Reach the mailbox</h3> | |||
| <p>The finished postcard is printed and mailed as one coordinated campaign.</p> | |||
| <h3>We mail it out</h3> | |||
| <p>We print the postcard and deliver it to mailboxes in your area.</p> | |||
| </li> | |||
| </ol> | |||
| </div> | |||
| @@ -235,7 +267,7 @@ Response.CodePage = 65001 | |||
| <p>Tell us what you do and where you want to be seen. We will help you explore the right PostcardMojo campaign.</p> | |||
| </div> | |||
| <div class="cta-actions"> | |||
| <a class="button button-gold" href="mailto:info@postcardmojo.com?subject=PostcardMojo%209x12%20campaign%20inquiry">Request campaign details</a> | |||
| <a class="button button-gold" href="#contact">Request campaign details</a> | |||
| <span>Campaign availability varies by area.</span> | |||
| </div> | |||
| </div> | |||
| @@ -251,7 +283,7 @@ Response.CodePage = 65001 | |||
| <nav aria-label="Footer navigation"> | |||
| <a href="#how-it-works">How it works</a> | |||
| <a href="#faq">FAQ</a> | |||
| <a href="mailto:info@postcardmojo.com">Contact</a> | |||
| <a href="#contact">Contact</a> | |||
| </nav> | |||
| <small>© <%= Year(Date()) %> PostcardMojo. All rights reserved.</small> | |||
| </div> | |||
| @@ -159,7 +159,7 @@ | |||
| <virtualDirectory path="/" physicalPath="%ASPC_STARTER_ROOT%public" /> | |||
| </application> | |||
| <bindings> | |||
| <binding protocol="http" bindingInformation=":8080:localhost" /> | |||
| <binding protocol="http" bindingInformation=":8082:localhost" /> | |||
| </bindings> | |||
| </site> | |||
| <siteDefaults> | |||
| @@ -5,6 +5,7 @@ | |||
| router.AddRoute "GET", "/", "HomeController", "Index" | |||
| router.AddRoute "GET", "", "HomeController", "Index" | |||
| router.AddRoute "GET", "/home", "HomeController", "Index" | |||
| router.AddRoute "POST", "/contact", "HomeController", "Contact" | |||
| ' Dispatch the request (resolves route and executes controller action) | |||
| MVC.DispatchRequest Request.ServerVariables("REQUEST_METHOD"), _ | |||
| @@ -570,7 +570,7 @@ summary { | |||
| .steps { | |||
| position: relative; | |||
| display: grid; | |||
| grid-template-columns: repeat(4, 1fr); | |||
| grid-template-columns: repeat(3, 1fr); | |||
| gap: 18px; | |||
| margin: 0; | |||
| padding: 0; | |||
| @@ -1123,7 +1123,6 @@ summary { | |||
| gap: 30px; | |||
| } | |||
| .steps, | |||
| .fit-grid { | |||
| grid-template-columns: repeat(2, 1fr); | |||
| } | |||
| @@ -1428,3 +1427,120 @@ summary { | |||
| transition-duration: 0.01ms !important; | |||
| } | |||
| } | |||
| /* Contact card */ | |||
| .contact-card { | |||
| position: relative; | |||
| z-index: 2; | |||
| padding: clamp(22px, 3vw, 34px); | |||
| border-radius: 18px; | |||
| background: var(--navy); | |||
| color: var(--white); | |||
| box-shadow: 0 24px 60px rgba(5, 47, 79, 0.28); | |||
| } | |||
| .contact-card h2 { | |||
| margin: 0 0 18px; | |||
| font-family: var(--font-display); | |||
| font-size: clamp(1.5rem, 2.4vw, 2rem); | |||
| line-height: 1.15; | |||
| } | |||
| .contact-form { | |||
| display: grid; | |||
| grid-template-columns: repeat(2, minmax(0, 1fr)); | |||
| gap: 14px 16px; | |||
| } | |||
| .contact-form label { | |||
| display: grid; | |||
| gap: 6px; | |||
| font-size: 0.9rem; | |||
| font-weight: 600; | |||
| } | |||
| .contact-form input, | |||
| .contact-form textarea { | |||
| width: 100%; | |||
| padding: 11px 12px; | |||
| border: 1px solid rgba(255, 255, 255, 0.35); | |||
| border-radius: 8px; | |||
| background: var(--white); | |||
| color: var(--ink); | |||
| font: inherit; | |||
| } | |||
| .contact-wide, | |||
| .contact-errors, | |||
| .contact-form .button { | |||
| grid-column: 1 / -1; | |||
| } | |||
| .contact-form .button { | |||
| justify-self: start; | |||
| } | |||
| .contact-errors { | |||
| padding: 10px 14px; | |||
| border-radius: 8px; | |||
| background: #fde8e8; | |||
| color: #8a1c1c; | |||
| font-size: 0.92rem; | |||
| } | |||
| .contact-errors ul { | |||
| margin: 0; | |||
| padding-left: 18px; | |||
| } | |||
| .contact-success { | |||
| margin: 0; | |||
| color: var(--gold); | |||
| font-weight: 700; | |||
| } | |||
| .contact-trap { | |||
| position: absolute; | |||
| left: -9999px; | |||
| } | |||
| .hero .hero-grid { | |||
| width: min(1640px, calc(100% - 32px)); | |||
| grid-template-columns: minmax(0, 0.75fr) minmax(0, 1.6fr) minmax(280px, 0.75fr); | |||
| grid-template-areas: "copy card contact"; | |||
| align-items: center; | |||
| gap: clamp(20px, 2.5vw, 40px); | |||
| } | |||
| .hero-grid .hero-copy { grid-area: copy; } | |||
| .hero-grid .contact-card { grid-area: contact; } | |||
| .hero-grid .postcard-stage { grid-area: card; min-height: 0; } | |||
| .hero-grid .postcard { width: 100%; } | |||
| .hero-grid h1 { font-size: clamp(2rem, 3vw, 3.1rem); } | |||
| .hero-grid .contact-form { grid-template-columns: 1fr; gap: 12px; } | |||
| .hero-grid .stage-note { position: static; margin-top: 16px; max-width: none; } | |||
| @media (max-width: 1100px) { | |||
| .hero .hero-grid { | |||
| grid-template-columns: minmax(0, 1fr) minmax(300px, 0.9fr); | |||
| grid-template-areas: "copy contact" "card card"; | |||
| align-items: start; | |||
| } | |||
| .hero-grid .postcard { width: min(100%, 610px); } | |||
| .hero-grid .postcard-stage { min-height: 520px; } | |||
| } | |||
| @media (max-width: 700px) { | |||
| .hero .hero-grid { | |||
| grid-template-columns: 1fr; | |||
| grid-template-areas: "copy" "contact" "card"; | |||
| } | |||
| } | |||
| @media (max-width: 700px) { | |||
| .contact-form { | |||
| grid-template-columns: 1fr; | |||
| } | |||
| } | |||
| @@ -38,6 +38,13 @@ | |||
| <!-- Cache-bust parameter name (default: "v") --> | |||
| <add key="CacheBustParamName" value="v" /> | |||
| <!-- Contact form email (the mailbox password comes from SMTP_PASSWORD in the project-root .env file) --> | |||
| <add key="ContactRecipient" value="covington.daniel@gmail.com" /> | |||
| <add key="SmtpServer" value="netsol-smtp-oxcs.hostingplatform.com" /> | |||
| <add key="SmtpPort" value="465" /> | |||
| <add key="SmtpUsername" value="danielcovington@postcardmojo.com" /> | |||
| <add key="SmtpUseSsl" value="true" /> | |||
| </appSettings> | |||
| <system.webServer> | |||
Powered by TurnKey Linux.