mirror of
https://github.com/wahyd4/golink.git
synced 2026-08-09 05:05:56 +10:00
cmd/golink: provide user documentation
Add a new /.help URL which contains a first pass at user docs, covering naming rules for short links, using go templates, and basic API access. Also add client side validation for short name and a link to help page. Change-Id: I4b66793248eba17e567c4f422ee197617f74ee24
This commit is contained in:
@@ -47,7 +47,7 @@ var stats struct {
|
||||
//go:embed link-snapshot.json
|
||||
var lastSnapshot []byte
|
||||
|
||||
//go:embed *.html static
|
||||
//go:embed static tmpl/*.html
|
||||
var embeddedFS embed.FS
|
||||
|
||||
// db stores short links.
|
||||
@@ -89,6 +89,7 @@ func main() {
|
||||
|
||||
http.HandleFunc("/", serveGo)
|
||||
http.HandleFunc("/.export", serveExport)
|
||||
http.HandleFunc("/.help", serveHelp)
|
||||
http.Handle("/_/export", http.RedirectHandler("/.export", http.StatusMovedPermanently))
|
||||
http.Handle("/.static/", http.StripPrefix("/.", http.FileServer(http.FS(embeddedFS))))
|
||||
|
||||
@@ -120,23 +121,27 @@ func main() {
|
||||
}
|
||||
}
|
||||
|
||||
// homeCreate is the template used by the http://go/ index page where you can
|
||||
// homeTmpl is the template used by the http://go/ index page where you can
|
||||
// create or edit links.
|
||||
var homeCreate *template.Template
|
||||
var homeTmpl *template.Template
|
||||
|
||||
// helpTmpl is the template used by the http://go/.help page
|
||||
var helpTmpl *template.Template
|
||||
|
||||
type visitData struct {
|
||||
Short string
|
||||
NumClicks int
|
||||
}
|
||||
|
||||
// homeData is the data used by the homeCreate template.
|
||||
// homeData is the data used by the homeTmpl template.
|
||||
type homeData struct {
|
||||
Short string
|
||||
Clicks []visitData
|
||||
}
|
||||
|
||||
func init() {
|
||||
homeCreate = template.Must(template.ParseFS(embeddedFS, "home.html"))
|
||||
homeTmpl = template.Must(template.ParseFS(embeddedFS, "tmpl/base.html", "tmpl/home.html"))
|
||||
helpTmpl = template.Must(template.ParseFS(embeddedFS, "tmpl/base.html", "tmpl/help.html"))
|
||||
}
|
||||
|
||||
// initStats initializes the in-memory stats counter with counts from db.
|
||||
@@ -199,12 +204,16 @@ func serveHome(w http.ResponseWriter, short string) {
|
||||
return clicks[i].Short < clicks[j].Short
|
||||
})
|
||||
|
||||
homeCreate.Execute(w, homeData{
|
||||
homeTmpl.Execute(w, homeData{
|
||||
Short: html.EscapeString(short),
|
||||
Clicks: clicks,
|
||||
})
|
||||
}
|
||||
|
||||
func serveHelp(w http.ResponseWriter, r *http.Request) {
|
||||
helpTmpl.Execute(w, nil)
|
||||
}
|
||||
|
||||
func serveGo(w http.ResponseWriter, r *http.Request) {
|
||||
if r.RequestURI == "/" {
|
||||
switch r.Method {
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/forms": "^0.5.2",
|
||||
"@tailwindcss/typography": "^0.5.2",
|
||||
"tailwindcss": "^3.0.24"
|
||||
}
|
||||
}
|
||||
|
||||
+460
-8
@@ -671,6 +671,422 @@ select {
|
||||
}
|
||||
}
|
||||
|
||||
.prose {
|
||||
color: #333;
|
||||
max-width: 65ch;
|
||||
}
|
||||
|
||||
.prose :where([class~="lead"]):not(:where([class~="not-prose"] *)) {
|
||||
color: var(--tw-prose-lead);
|
||||
font-size: 1.25em;
|
||||
line-height: 1.6;
|
||||
margin-top: 1.2em;
|
||||
margin-bottom: 1.2em;
|
||||
}
|
||||
|
||||
.prose :where(a):not(:where([class~="not-prose"] *)) {
|
||||
color: #3182ce;
|
||||
text-decoration: underline;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.prose :where(a):not(:where([class~="not-prose"] *)):hover {
|
||||
color: #2c5282;
|
||||
}
|
||||
|
||||
.prose :where(strong):not(:where([class~="not-prose"] *)) {
|
||||
color: var(--tw-prose-bold);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.prose :where(ol):not(:where([class~="not-prose"] *)) {
|
||||
list-style-type: decimal;
|
||||
padding-left: 1.625em;
|
||||
}
|
||||
|
||||
.prose :where(ol[type="A"]):not(:where([class~="not-prose"] *)) {
|
||||
list-style-type: upper-alpha;
|
||||
}
|
||||
|
||||
.prose :where(ol[type="a"]):not(:where([class~="not-prose"] *)) {
|
||||
list-style-type: lower-alpha;
|
||||
}
|
||||
|
||||
.prose :where(ol[type="A" s]):not(:where([class~="not-prose"] *)) {
|
||||
list-style-type: upper-alpha;
|
||||
}
|
||||
|
||||
.prose :where(ol[type="a" s]):not(:where([class~="not-prose"] *)) {
|
||||
list-style-type: lower-alpha;
|
||||
}
|
||||
|
||||
.prose :where(ol[type="I"]):not(:where([class~="not-prose"] *)) {
|
||||
list-style-type: upper-roman;
|
||||
}
|
||||
|
||||
.prose :where(ol[type="i"]):not(:where([class~="not-prose"] *)) {
|
||||
list-style-type: lower-roman;
|
||||
}
|
||||
|
||||
.prose :where(ol[type="I" s]):not(:where([class~="not-prose"] *)) {
|
||||
list-style-type: upper-roman;
|
||||
}
|
||||
|
||||
.prose :where(ol[type="i" s]):not(:where([class~="not-prose"] *)) {
|
||||
list-style-type: lower-roman;
|
||||
}
|
||||
|
||||
.prose :where(ol[type="1"]):not(:where([class~="not-prose"] *)) {
|
||||
list-style-type: decimal;
|
||||
}
|
||||
|
||||
.prose :where(ul):not(:where([class~="not-prose"] *)) {
|
||||
list-style-type: disc;
|
||||
padding-left: 1.625em;
|
||||
}
|
||||
|
||||
.prose :where(ol > li):not(:where([class~="not-prose"] *))::marker {
|
||||
font-weight: 400;
|
||||
color: var(--tw-prose-counters);
|
||||
}
|
||||
|
||||
.prose :where(ul > li):not(:where([class~="not-prose"] *))::marker {
|
||||
color: var(--tw-prose-bullets);
|
||||
}
|
||||
|
||||
.prose :where(hr):not(:where([class~="not-prose"] *)) {
|
||||
border-color: var(--tw-prose-hr);
|
||||
border-top-width: 1px;
|
||||
margin-top: 3em;
|
||||
margin-bottom: 3em;
|
||||
}
|
||||
|
||||
.prose :where(blockquote):not(:where([class~="not-prose"] *)) {
|
||||
font-weight: 500;
|
||||
font-style: italic;
|
||||
color: var(--tw-prose-quotes);
|
||||
border-left-width: 0.25rem;
|
||||
border-left-color: var(--tw-prose-quote-borders);
|
||||
quotes: "\201C""\201D""\2018""\2019";
|
||||
margin-top: 1.6em;
|
||||
margin-bottom: 1.6em;
|
||||
padding-left: 1em;
|
||||
}
|
||||
|
||||
.prose :where(blockquote p:first-of-type):not(:where([class~="not-prose"] *))::before {
|
||||
content: open-quote;
|
||||
}
|
||||
|
||||
.prose :where(blockquote p:last-of-type):not(:where([class~="not-prose"] *))::after {
|
||||
content: close-quote;
|
||||
}
|
||||
|
||||
.prose :where(h1):not(:where([class~="not-prose"] *)) {
|
||||
color: var(--tw-prose-headings);
|
||||
font-weight: 800;
|
||||
font-size: 2.25em;
|
||||
margin-top: 0;
|
||||
margin-bottom: 0.8888889em;
|
||||
line-height: 1.1111111;
|
||||
}
|
||||
|
||||
.prose :where(h1 strong):not(:where([class~="not-prose"] *)) {
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.prose :where(h2):not(:where([class~="not-prose"] *)) {
|
||||
color: var(--tw-prose-headings);
|
||||
font-weight: 700;
|
||||
font-size: 1.5em;
|
||||
margin-top: 2em;
|
||||
margin-bottom: 1em;
|
||||
line-height: 1.3333333;
|
||||
}
|
||||
|
||||
.prose :where(h2 strong):not(:where([class~="not-prose"] *)) {
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.prose :where(h3):not(:where([class~="not-prose"] *)) {
|
||||
color: var(--tw-prose-headings);
|
||||
font-weight: 600;
|
||||
font-size: 1.25em;
|
||||
margin-top: 1.6em;
|
||||
margin-bottom: 0.6em;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.prose :where(h3 strong):not(:where([class~="not-prose"] *)) {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.prose :where(h4):not(:where([class~="not-prose"] *)) {
|
||||
color: var(--tw-prose-headings);
|
||||
font-weight: 600;
|
||||
margin-top: 1.5em;
|
||||
margin-bottom: 0.5em;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.prose :where(h4 strong):not(:where([class~="not-prose"] *)) {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.prose :where(figure > *):not(:where([class~="not-prose"] *)) {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.prose :where(figcaption):not(:where([class~="not-prose"] *)) {
|
||||
color: var(--tw-prose-captions);
|
||||
font-size: 0.875em;
|
||||
line-height: 1.4285714;
|
||||
margin-top: 0.8571429em;
|
||||
}
|
||||
|
||||
.prose :where(code):not(:where([class~="not-prose"] *)) {
|
||||
color: var(--tw-prose-code);
|
||||
font-weight: 600;
|
||||
font-size: 0.875em;
|
||||
}
|
||||
|
||||
.prose :where(code):not(:where([class~="not-prose"] *))::before {
|
||||
content: ;
|
||||
}
|
||||
|
||||
.prose :where(code):not(:where([class~="not-prose"] *))::after {
|
||||
content: ;
|
||||
}
|
||||
|
||||
.prose :where(a code):not(:where([class~="not-prose"] *)) {
|
||||
color: var(--tw-prose-links);
|
||||
}
|
||||
|
||||
.prose :where(pre):not(:where([class~="not-prose"] *)) {
|
||||
color: var(--tw-prose-pre-code);
|
||||
background-color: var(--tw-prose-pre-bg);
|
||||
overflow-x: auto;
|
||||
font-weight: 400;
|
||||
font-size: 0.875em;
|
||||
line-height: 1.7142857;
|
||||
margin-top: 1.7142857em;
|
||||
margin-bottom: 1.7142857em;
|
||||
border-radius: 0.375rem;
|
||||
padding-top: 0.8571429em;
|
||||
padding-right: 1.1428571em;
|
||||
padding-bottom: 0.8571429em;
|
||||
padding-left: 1.1428571em;
|
||||
}
|
||||
|
||||
.prose :where(pre code):not(:where([class~="not-prose"] *)) {
|
||||
background-color: transparent;
|
||||
border-width: 0;
|
||||
border-radius: 0;
|
||||
padding: 0;
|
||||
font-weight: inherit;
|
||||
color: inherit;
|
||||
font-size: inherit;
|
||||
font-family: inherit;
|
||||
line-height: inherit;
|
||||
}
|
||||
|
||||
.prose :where(pre code):not(:where([class~="not-prose"] *))::before {
|
||||
content: none;
|
||||
}
|
||||
|
||||
.prose :where(pre code):not(:where([class~="not-prose"] *))::after {
|
||||
content: none;
|
||||
}
|
||||
|
||||
.prose :where(table):not(:where([class~="not-prose"] *)) {
|
||||
width: 100%;
|
||||
table-layout: auto;
|
||||
text-align: left;
|
||||
margin-top: 2em;
|
||||
margin-bottom: 2em;
|
||||
font-size: 0.875em;
|
||||
line-height: 1.7142857;
|
||||
}
|
||||
|
||||
.prose :where(thead):not(:where([class~="not-prose"] *)) {
|
||||
border-bottom-width: 1px;
|
||||
border-bottom-color: var(--tw-prose-th-borders);
|
||||
}
|
||||
|
||||
.prose :where(thead th):not(:where([class~="not-prose"] *)) {
|
||||
color: var(--tw-prose-headings);
|
||||
font-weight: 600;
|
||||
vertical-align: bottom;
|
||||
padding-right: 0.5714286em;
|
||||
padding-bottom: 0.5714286em;
|
||||
padding-left: 0.5714286em;
|
||||
}
|
||||
|
||||
.prose :where(tbody tr):not(:where([class~="not-prose"] *)) {
|
||||
border-bottom-width: 1px;
|
||||
border-bottom-color: var(--tw-prose-td-borders);
|
||||
}
|
||||
|
||||
.prose :where(tbody tr:last-child):not(:where([class~="not-prose"] *)) {
|
||||
border-bottom-width: 0;
|
||||
}
|
||||
|
||||
.prose :where(tbody td):not(:where([class~="not-prose"] *)) {
|
||||
vertical-align: baseline;
|
||||
padding-top: 0.5714286em;
|
||||
padding-right: 0.5714286em;
|
||||
padding-bottom: 0.5714286em;
|
||||
padding-left: 0.5714286em;
|
||||
}
|
||||
|
||||
.prose {
|
||||
--tw-prose-body: #374151;
|
||||
--tw-prose-headings: #111827;
|
||||
--tw-prose-lead: #4b5563;
|
||||
--tw-prose-links: #111827;
|
||||
--tw-prose-bold: #111827;
|
||||
--tw-prose-counters: #6b7280;
|
||||
--tw-prose-bullets: #d1d5db;
|
||||
--tw-prose-hr: #e5e7eb;
|
||||
--tw-prose-quotes: #111827;
|
||||
--tw-prose-quote-borders: #e5e7eb;
|
||||
--tw-prose-captions: #6b7280;
|
||||
--tw-prose-code: #111827;
|
||||
--tw-prose-pre-code: #e5e7eb;
|
||||
--tw-prose-pre-bg: #1f2937;
|
||||
--tw-prose-th-borders: #d1d5db;
|
||||
--tw-prose-td-borders: #e5e7eb;
|
||||
--tw-prose-invert-body: #d1d5db;
|
||||
--tw-prose-invert-headings: #fff;
|
||||
--tw-prose-invert-lead: #9ca3af;
|
||||
--tw-prose-invert-links: #fff;
|
||||
--tw-prose-invert-bold: #fff;
|
||||
--tw-prose-invert-counters: #9ca3af;
|
||||
--tw-prose-invert-bullets: #4b5563;
|
||||
--tw-prose-invert-hr: #374151;
|
||||
--tw-prose-invert-quotes: #f3f4f6;
|
||||
--tw-prose-invert-quote-borders: #374151;
|
||||
--tw-prose-invert-captions: #9ca3af;
|
||||
--tw-prose-invert-code: #fff;
|
||||
--tw-prose-invert-pre-code: #d1d5db;
|
||||
--tw-prose-invert-pre-bg: rgb(0 0 0 / 50%);
|
||||
--tw-prose-invert-th-borders: #4b5563;
|
||||
--tw-prose-invert-td-borders: #374151;
|
||||
font-size: 1rem;
|
||||
line-height: 1.75;
|
||||
}
|
||||
|
||||
.prose :where(p):not(:where([class~="not-prose"] *)) {
|
||||
margin-top: 1.25em;
|
||||
margin-bottom: 1.25em;
|
||||
}
|
||||
|
||||
.prose :where(img):not(:where([class~="not-prose"] *)) {
|
||||
margin-top: 2em;
|
||||
margin-bottom: 2em;
|
||||
}
|
||||
|
||||
.prose :where(video):not(:where([class~="not-prose"] *)) {
|
||||
margin-top: 2em;
|
||||
margin-bottom: 2em;
|
||||
}
|
||||
|
||||
.prose :where(figure):not(:where([class~="not-prose"] *)) {
|
||||
margin-top: 2em;
|
||||
margin-bottom: 2em;
|
||||
}
|
||||
|
||||
.prose :where(h2 code):not(:where([class~="not-prose"] *)) {
|
||||
font-size: 0.875em;
|
||||
}
|
||||
|
||||
.prose :where(h3 code):not(:where([class~="not-prose"] *)) {
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.prose :where(li):not(:where([class~="not-prose"] *)) {
|
||||
margin-top: 0.5em;
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
|
||||
.prose :where(ol > li):not(:where([class~="not-prose"] *)) {
|
||||
padding-left: 0.375em;
|
||||
}
|
||||
|
||||
.prose :where(ul > li):not(:where([class~="not-prose"] *)) {
|
||||
padding-left: 0.375em;
|
||||
}
|
||||
|
||||
.prose > :where(ul > li p):not(:where([class~="not-prose"] *)) {
|
||||
margin-top: 0.75em;
|
||||
margin-bottom: 0.75em;
|
||||
}
|
||||
|
||||
.prose > :where(ul > li > *:first-child):not(:where([class~="not-prose"] *)) {
|
||||
margin-top: 1.25em;
|
||||
}
|
||||
|
||||
.prose > :where(ul > li > *:last-child):not(:where([class~="not-prose"] *)) {
|
||||
margin-bottom: 1.25em;
|
||||
}
|
||||
|
||||
.prose > :where(ol > li > *:first-child):not(:where([class~="not-prose"] *)) {
|
||||
margin-top: 1.25em;
|
||||
}
|
||||
|
||||
.prose > :where(ol > li > *:last-child):not(:where([class~="not-prose"] *)) {
|
||||
margin-bottom: 1.25em;
|
||||
}
|
||||
|
||||
.prose :where(ul ul, ul ol, ol ul, ol ol):not(:where([class~="not-prose"] *)) {
|
||||
margin-top: 0.75em;
|
||||
margin-bottom: 0.75em;
|
||||
}
|
||||
|
||||
.prose :where(hr + *):not(:where([class~="not-prose"] *)) {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.prose :where(h2 + *):not(:where([class~="not-prose"] *)) {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.prose :where(h3 + *):not(:where([class~="not-prose"] *)) {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.prose :where(h4 + *):not(:where([class~="not-prose"] *)) {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.prose :where(thead th:first-child):not(:where([class~="not-prose"] *)) {
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.prose :where(thead th:last-child):not(:where([class~="not-prose"] *)) {
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
.prose :where(tbody td:first-child):not(:where([class~="not-prose"] *)) {
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.prose :where(tbody td:last-child):not(:where([class~="not-prose"] *)) {
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
.prose > :where(:first-child):not(:where([class~="not-prose"] *)) {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.prose > :where(:last-child):not(:where([class~="not-prose"] *)) {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.absolute {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.m-2 {
|
||||
margin: 0.5rem;
|
||||
}
|
||||
@@ -680,6 +1096,11 @@ select {
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.mx-4 {
|
||||
margin-left: 1rem;
|
||||
margin-right: 1rem;
|
||||
}
|
||||
|
||||
.my-2 {
|
||||
margin-top: 0.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
@@ -693,6 +1114,10 @@ select {
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
.mr-2 {
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
.block {
|
||||
display: block;
|
||||
}
|
||||
@@ -713,10 +1138,18 @@ select {
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.max-w-4xl {
|
||||
max-width: 56rem;
|
||||
}
|
||||
|
||||
.max-w-full {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.max-w-5xl {
|
||||
max-width: 64rem;
|
||||
}
|
||||
|
||||
.flex-1 {
|
||||
flex: 1 1 0%;
|
||||
}
|
||||
@@ -759,14 +1192,14 @@ select {
|
||||
border-bottom-width: 1px;
|
||||
}
|
||||
|
||||
.border-r-0 {
|
||||
border-right-width: 0px;
|
||||
}
|
||||
|
||||
.border-t {
|
||||
border-top-width: 1px;
|
||||
}
|
||||
|
||||
.border-r-0 {
|
||||
border-right-width: 0px;
|
||||
}
|
||||
|
||||
.border-gray-200 {
|
||||
--tw-border-opacity: 1;
|
||||
border-color: rgb(230 228 226 / var(--tw-border-opacity));
|
||||
@@ -823,6 +1256,10 @@ select {
|
||||
padding-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.pt-2 {
|
||||
padding-top: 0.5rem;
|
||||
}
|
||||
|
||||
.pt-6 {
|
||||
padding-top: 1.5rem;
|
||||
}
|
||||
@@ -831,10 +1268,6 @@ select {
|
||||
padding-right: 2rem;
|
||||
}
|
||||
|
||||
.pt-2 {
|
||||
padding-top: 0.5rem;
|
||||
}
|
||||
|
||||
.text-left {
|
||||
text-align: left;
|
||||
}
|
||||
@@ -862,6 +1295,11 @@ select {
|
||||
line-height: 1rem;
|
||||
}
|
||||
|
||||
.text-sm {
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.25rem;
|
||||
}
|
||||
|
||||
.font-bold {
|
||||
font-weight: 700;
|
||||
}
|
||||
@@ -889,6 +1327,20 @@ select {
|
||||
color: rgb(159 153 149 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.text-blue-200 {
|
||||
color: rgba(43, 123, 185, 0.15);
|
||||
}
|
||||
|
||||
.text-blue-500 {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(74 125 221 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.text-blue-600 {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(77 120 200 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.placeholder\:text-gray-400::-moz-placeholder {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(182 176 173 / var(--tw-text-opacity));
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 25 KiB |
+16
-1
@@ -1,5 +1,5 @@
|
||||
module.exports = {
|
||||
content: ["./*.html"],
|
||||
content: ["./tmpl/*.html"],
|
||||
theme: {
|
||||
colors: {
|
||||
blue: {
|
||||
@@ -27,8 +27,23 @@ module.exports = {
|
||||
white: '#fff',
|
||||
current: 'currentColor',
|
||||
},
|
||||
extend: {
|
||||
typography: {
|
||||
DEFAULT: {
|
||||
css: {
|
||||
'code::before': {
|
||||
'content': '',
|
||||
},
|
||||
'code::after': {
|
||||
'content': '',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [
|
||||
require('@tailwindcss/forms'),
|
||||
require('@tailwindcss/typography'),
|
||||
],
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<html>
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>go/</title>
|
||||
<link rel="stylesheet" href="/.static/base.css">
|
||||
@@ -7,39 +8,13 @@
|
||||
<body class="flex flex-col min-h-screen">
|
||||
<div class="bg-gray-100 border-b border-gray-200 pt-4 pb-2 mb-6">
|
||||
<header class="container mx-auto px-4">
|
||||
<h1 class="text-2xl font-bold pb-1">go/</h1>
|
||||
<h1 class="text-2xl font-bold pb-1"><a href="/">go/</a></h1>
|
||||
A private shortlink service for your tailnet
|
||||
</header>
|
||||
</div>
|
||||
|
||||
<main class="container mx-auto px-4 flex-1">
|
||||
<h2 class="text-xl font-bold pb-2">Create a new link</h2>
|
||||
|
||||
<form method="POST" action="/" class="flex flex-wrap">
|
||||
<div class="flex">
|
||||
<label for=short class="flex my-2 px-2 items-center bg-gray-100 border border-r-0 border-gray-300 rounded-l-md text-gray-700">http://go/</label>
|
||||
<input id=short name=short required type=text size=15 placeholder="shortname" value="{{.Short}}"
|
||||
class="p-2 my-2 rounded-r-md border-gray-300 placeholder:text-gray-400">
|
||||
<span class="flex m-2 items-center">→</span>
|
||||
</div>
|
||||
<input name=long required type=text size=40 placeholder="https://destination-url"{{if .Short}} autofocus{{end}} class="p-2 my-2 max-w-full rounded-md border-gray-300 placeholder:text-gray-400">
|
||||
<button type=submit class="py-2 px-4 m-2 rounded-md bg-blue-500 border-blue-500 text-white hover:bg-blue-600 hover:border-blue-600">Create</button>
|
||||
</form>
|
||||
|
||||
<h2 class="text-xl font-bold pt-6 pb-2">Popular Links</h2>
|
||||
<table class="table-auto ">
|
||||
<thead class="border-b border-gray-200 uppercase text-xs text-gray-500 text-left">
|
||||
<tr>
|
||||
<th class="p-2">Link</th>
|
||||
<th class="p-2">Clicks</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{range .Clicks}}
|
||||
<tr class="hover:bg-gray-100 border-b border-gray-200"><td><a class="block p-2 pr-8 hover:text-blue-500 hover:underline" href="http://go/{{.Short}}">go/{{.Short}}</a></td><td class="p-2">{{.NumClicks}}</td></tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
{{ block "main" .}}{{ end }}
|
||||
</main>
|
||||
<footer class="bg-gray-100 border-t border-gray-200 pt-2 pb-2 mt-6">
|
||||
<div class="container mx-auto px-4 leading-6 text-right">
|
||||
+129
@@ -0,0 +1,129 @@
|
||||
{{ define "main" }}
|
||||
<article class="prose max-w-5xl">
|
||||
<p>
|
||||
go links provide short, memorable links for the websites you and your team use most.
|
||||
|
||||
<h2>Creating go links</h2>
|
||||
|
||||
<p>
|
||||
All go links have a <strong>short name</strong> and a <strong>destination link</strong> that the go link points to.
|
||||
Some notes on short names:
|
||||
|
||||
<ul>
|
||||
<li>names must start with a letter or number
|
||||
<li>names may contain letters, numbers, hyphens, and periods
|
||||
<li>names are <strong>not</strong> case-sensitive (go/foo is the same as go/FOO)
|
||||
<li>hyphens are ignored when resolving links (go/meetingnotes is the same as go/meeting-notes)
|
||||
</ul>
|
||||
|
||||
<p>
|
||||
In simple cases, the destination link is an absolute URL, such as <strong>https://www.google.com/</strong>.
|
||||
|
||||
<p>
|
||||
<img width=737 height=62 class="mx-4" src="/.static/images/create-link.png">
|
||||
|
||||
<h2>Resolving links</h2>
|
||||
|
||||
<p>
|
||||
When logged in to your Tailscale network, go links can be entered directly into any browser or command line utility such as curl.
|
||||
You do not need any additional browser extensions.
|
||||
|
||||
<p>
|
||||
Any additional path provided after the short name will be added to the end of the destination link.
|
||||
For example, if <strong>go/who</strong> goes to your company directory at <strong>http://directory/</strong>,
|
||||
then <strong>go/who/amelie</strong> will go to <strong>http://directory/amelie</strong>.
|
||||
|
||||
<p>
|
||||
<a href="#advanced">Advanced destination links</a> allow you to further customize this behavior.
|
||||
|
||||
<h2 id="advanced">Advanced destination links</h2>
|
||||
|
||||
<p>
|
||||
To have more control over how go links are resolved, destination links can use <a href="https://pkg.go.dev/text/template">Go template syntax</a>.
|
||||
Templates are provided a data structure with the following fields:
|
||||
|
||||
<ul>
|
||||
<li><code>.Path</code> is the remaining path value after the short name (without a leading slash). For the link <strong>go/who/amelie</strong>,
|
||||
the value of <code>.Path</code> is <code>amelie</code>.
|
||||
<li><code>.Now</code> is a <a href="https://pkg.go.dev/time#Time">time.Time</a> value representing the current date and time.
|
||||
Use <a href="https://pkg.go.dev/time#Time.Format">Time.Format</a> to output date or time in desired format.
|
||||
</ul>
|
||||
|
||||
Templates also have access to the following template functions:
|
||||
|
||||
<ul>
|
||||
<li><code>PathEscape</code> is the <a href="https://pkg.go.dev/net/url#PathEscape">url.PathEscape</a> function for escaping values inside a URL path.
|
||||
<li><code>QueryEscape</code> is the <a href="https://pkg.go.dev/net/url#QueryEscape">url.QueryEscape</a> function for escaping values inside a URL query.
|
||||
</ul>
|
||||
|
||||
<p>
|
||||
The most common use of advanced destination links is to put the additional path in a custom location in the destination link.
|
||||
For example, you might set the destination for <strong>go/search</strong> to:
|
||||
|
||||
<pre>{{`https://www.google.com/{{if .Path}}search?q={{QueryEscape .Path}}{{end}}`}}</pre>
|
||||
|
||||
When a user visits <strong>go/search</strong> with no additional path, they will be directed to <a href="https://www.google.com/">https://www.google.com/</a>.
|
||||
If they include an additional path like <strong>go/search/pangolins</strong>, they will be directed to <a href="https://www.google.com/search?q=pangolins">https://www.google.com/search?q=pangolins</a>.
|
||||
|
||||
<h3>Examples</h3>
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<td>Include path in query</td>
|
||||
<td>go/search</td>
|
||||
<td>{{`https://cloudsearch.google.com/{{if .Path}}cloudsearch/search?q={{QueryEscape .Path}}{{end}}`}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Include path in destination path</td>
|
||||
<td>go/slack</td>
|
||||
<td>{{`https://company.slack.com/{{if .Path}}channels/{{PathEscape .Path}}{{end}}`}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Include path in hostname</td>
|
||||
<td>go/varz</td>
|
||||
<td>{{`http://{{if .Path}}{{.Path}}{{else}}host{{end}}.example/debug/varz`}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Include today's date in wiki page</td>
|
||||
<td>go/today</td>
|
||||
<td>{{`http://wiki/{{.Now.Format "01-02-2006"}}`}}</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<h2 id="api">Application Programming Interface (API)</h2>
|
||||
|
||||
<p>
|
||||
There is no formal API, but many endpoints lend themselves to programmatic access.
|
||||
|
||||
<p>
|
||||
Include a "+" after a link to get information about a link without resolving it:
|
||||
|
||||
<pre>{{`$ curl go/search+
|
||||
{
|
||||
"Short": "search",
|
||||
"Long": "https://cloudsearch.google.com/{{if .Path}}cloudsearch/search?q={{QueryEscape .Path}}{{end}}",
|
||||
"Created": "2022-06-08T04:27:32.829906577Z",
|
||||
"LastEdit": "2022-06-13T04:42:08.396702416Z",
|
||||
"Owner": "amelie@company.com",
|
||||
"Clicks": 8
|
||||
}`}}
|
||||
</pre>
|
||||
|
||||
<p>
|
||||
Visit <a href="http://go/.export">go/.export</a> to export all saved links and their metadata in <a href="https://jsonlines.org/">JSON Lines format</a>.
|
||||
This is useful to create data snapshots that can be restored later.
|
||||
|
||||
<pre>{{`$ curl go/.export
|
||||
{"Short":"go","Long":"http://go","Created":"2022-05-31T13:04:44.741457796-07:00","LastEdit":"2022-05-31T13:04:44.741457796-07:00","Owner":"amelie@example.com","Clicks":1}
|
||||
{"Short":"slack","Long":"https://company.slack.com/{{if .Path}}channels/{{PathEscape .Path}}{{end}}","Created":"2022-06-17T18:05:43.562948451Z","LastEdit":"2022-06-17T18:06:35.811398Z","Owner":"amelie@example.com","Clicks":4}`}}
|
||||
</pre>
|
||||
|
||||
<p>
|
||||
Create a new link by sending a POST request with a <code>short</code> and <code>long</code> value:
|
||||
|
||||
<pre>{{`$ curl -d short=cs -d long=https://cs.github.com/ go
|
||||
{"Short":"cs","Long":"https://cs.github.com/","Created":"2022-06-03T22:15:29.993978392Z","LastEdit":"2022-06-03T22:15:29.993978392Z","Owner":"amelie@example.com"}`}}
|
||||
</pre>
|
||||
|
||||
</article>
|
||||
{{ end }}
|
||||
@@ -0,0 +1,30 @@
|
||||
{{ define "main" }}
|
||||
<h2 class="text-xl font-bold pb-2">Create a new link</h2>
|
||||
|
||||
<form method="POST" action="/" class="flex flex-wrap">
|
||||
<div class="flex">
|
||||
<label for=short class="flex my-2 px-2 items-center bg-gray-100 border border-r-0 border-gray-300 rounded-l-md text-gray-700">http://go/</label>
|
||||
<input id=short name=short required type=text size=15 placeholder="shortname" value="{{.Short}}" pattern="\w[\w\-\.]*" title="Must start with letter or number; may contain letters, numbers, dashes, and periods."
|
||||
class="p-2 my-2 rounded-r-md border-gray-300 placeholder:text-gray-400">
|
||||
<span class="flex m-2 items-center">→</span>
|
||||
</div>
|
||||
<input name=long required type=text size=40 placeholder="https://destination-url"{{if .Short}} autofocus{{end}} class="p-2 my-2 mr-2 max-w-full rounded-md border-gray-300 placeholder:text-gray-400">
|
||||
<button type=submit class="py-2 px-4 my-2 rounded-md bg-blue-500 border-blue-500 text-white hover:bg-blue-600 hover:border-blue-600">Create</button>
|
||||
</form>
|
||||
<p class="text-sm text-gray-500"><a class="text-blue-600 hover:underline" href="/.help">Help and advanced options</a></p>
|
||||
|
||||
<h2 class="text-xl font-bold pt-6 pb-2">Popular Links</h2>
|
||||
<table class="table-auto ">
|
||||
<thead class="border-b border-gray-200 uppercase text-xs text-gray-500 text-left">
|
||||
<tr>
|
||||
<th class="p-2">Link</th>
|
||||
<th class="p-2">Clicks</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{range .Clicks}}
|
||||
<tr class="hover:bg-gray-100 border-b border-gray-200"><td><a class="block p-2 pr-8 hover:text-blue-500 hover:underline" href="http://go/{{.Short}}">go/{{.Short}}</a></td><td class="p-2">{{.NumClicks}}</td></tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
{{ end }}
|
||||
@@ -30,6 +30,15 @@
|
||||
dependencies:
|
||||
mini-svg-data-uri "^1.2.3"
|
||||
|
||||
"@tailwindcss/typography@^0.5.2":
|
||||
version "0.5.2"
|
||||
resolved "https://registry.yarnpkg.com/@tailwindcss/typography/-/typography-0.5.2.tgz#24b069dab24d7a2467d01aca0dd432cb4b29f0ee"
|
||||
integrity sha512-coq8DBABRPFcVhVIk6IbKyyHUt7YTEC/C992tatFB+yEx5WGBQrCgsSFjxHUr8AWXphWckadVJbominEduYBqw==
|
||||
dependencies:
|
||||
lodash.castarray "^4.4.0"
|
||||
lodash.isplainobject "^4.0.6"
|
||||
lodash.merge "^4.6.2"
|
||||
|
||||
acorn-node@^1.8.2:
|
||||
version "1.8.2"
|
||||
resolved "https://registry.yarnpkg.com/acorn-node/-/acorn-node-1.8.2.tgz#114c95d64539e53dede23de8b9d96df7c7ae2af8"
|
||||
@@ -220,6 +229,21 @@ lilconfig@^2.0.5:
|
||||
resolved "https://registry.yarnpkg.com/lilconfig/-/lilconfig-2.0.5.tgz#19e57fd06ccc3848fd1891655b5a447092225b25"
|
||||
integrity sha512-xaYmXZtTHPAw5m+xLN8ab9C+3a8YmV3asNSPOATITbtwrfbwaLJj8h66H1WMIpALCkqsIzK3h7oQ+PdX+LQ9Eg==
|
||||
|
||||
lodash.castarray@^4.4.0:
|
||||
version "4.4.0"
|
||||
resolved "https://registry.yarnpkg.com/lodash.castarray/-/lodash.castarray-4.4.0.tgz#c02513515e309daddd4c24c60cfddcf5976d9115"
|
||||
integrity sha512-aVx8ztPv7/2ULbArGJ2Y42bG1mEQ5mGjpdvrbJcJFU3TbYybe+QlLS4pst9zV52ymy2in1KpFPiZnAOATxD4+Q==
|
||||
|
||||
lodash.isplainobject@^4.0.6:
|
||||
version "4.0.6"
|
||||
resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb"
|
||||
integrity sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==
|
||||
|
||||
lodash.merge@^4.6.2:
|
||||
version "4.6.2"
|
||||
resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a"
|
||||
integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==
|
||||
|
||||
merge2@^1.3.0:
|
||||
version "1.4.1"
|
||||
resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae"
|
||||
|
||||
Reference in New Issue
Block a user