├── .gitignore ├── app.yaml ├── static ├── hn.min.js └── hn.js ├── README.md └── hnbutton ├── button.html └── hnbutton.go /.gitignore: -------------------------------------------------------------------------------- 1 | *.6 2 | *.8 3 | *.o 4 | *.so 5 | *.cgo?.* 6 | _cgo_* 7 | _test* 8 | *.out 9 | _obj 10 | -------------------------------------------------------------------------------- /app.yaml: -------------------------------------------------------------------------------- 1 | application: hnbutton 2 | version: 4 3 | runtime: go 4 | api_version: go1 5 | 6 | handlers: 7 | - url: /static 8 | static_dir: static 9 | 10 | - url: /.* 11 | script: _go_app 12 | -------------------------------------------------------------------------------- /static/hn.min.js: -------------------------------------------------------------------------------- 1 | (function(a){"use strict";var b,c=a.document,d=function(a,b){if(c.getElementsByClassName)return c.getElementsByClassName(a);var d=[],e=c.getElementsByTagName(b||"*"),f,g;a=" "+a+" ";for(f=0;f-1&&d.push(g);return d},e=d("hn-share-button","a"),f=a.addEventListener?"addEventListener":"attachEvent",g=a[f],h=f==="attachEvent"?"onmessage":"message",i="http://hnbutton.appspot.com/";a._gaq||(a._gaq=[]),g(h,function(b){b.origin===i&&(b.data==="vote"||b.data==="submit")&&a._gaq.push(["_trackSocial","Hacker News",b.data])},!1);for(b=e.length-1;b>=0;b--){var j=e[b],k=j.getAttribute("data-title")||c.title,l=j.getAttribute("data-url")||a.location.href,m=c.createElement("iframe");m.src=i+"button?title="+encodeURIComponent(k)+"&url="+encodeURIComponent(l),m.scrolling="auto",m.frameBorder="0",m.width="75px",m.height="20px",m.className="hn-share-iframe",j.parentNode.insertBefore(m,j),j.parentNode.removeChild(j)}})(window) -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Embeddable Hacker News vote / counter button 2 | 3 | ![HN Button](http://img.skitch.com/20120415-bp8igiq74w53f91swt6tcy9cx8.jpg) 4 | 5 | Async, embeddable submit + vote counter button for Hacker News. 6 | 7 | - If the story has not been posted to HN, "Submit" button is shown, otherwise latest point count is displayed. 8 | - Auto-detects Google Analytics and registers clicks events (see reports under `Traffic Sources > Social > Social Plugins`). 9 | 10 | ### Embedding the button 11 | 12 | **Step 1**, place the HN link where you want the button appear on the page: 13 | 14 | ```html 15 | 16 | Vote on HN 17 | 18 | 19 | Vote on HN 20 | ``` 21 | 22 | **Step 2**, add the following loader snippet right before the `` tag: 23 | 24 | ```html 25 | 32 | ``` 33 | 34 | _Note: you can safely embed multiple buttons on the same page._ 35 | 36 | ### Misc 37 | 38 | * Kudos to @sbashyal and @stbullard for the button styling (hnlike.com) 39 | * (MIT License) - Copyright (c) 2012 Ilya Grigorik -------------------------------------------------------------------------------- /static/hn.js: -------------------------------------------------------------------------------- 1 | /*jshint forin:true, noarg:true, noempty:true, eqeqeq:true, bitwise:true, strict:true, undef:true, curly:true, browser:true, indent:2, maxerr:50, expr:true */ 2 | (function (w) { 3 | "use strict"; 4 | var j, 5 | d = w.document, 6 | getElementsByClassName = function(match, tag) { 7 | if (d.getElementsByClassName) { 8 | return d.getElementsByClassName(match); 9 | } 10 | var result = [], 11 | elements = d.getElementsByTagName(tag || '*'), 12 | i, elem; 13 | match = " " + match + " "; 14 | for (i = 0; i < elements.length; i++) { 15 | elem = elements[i]; 16 | if ((" " + (elem.className || elem.getAttribute("class")) + " ").indexOf(match) > -1) { 17 | result.push(elem); 18 | } 19 | } 20 | return result; 21 | }, 22 | hnAnchorElements = getElementsByClassName("hn-share-button", "a"), 23 | eventMethod = w.addEventListener ? "addEventListener" : "attachEvent", 24 | eventer = w[eventMethod], 25 | messageEvent = eventMethod === "attachEvent" ? "onmessage" : "message", 26 | base = "http://hnbutton.appspot.com/"; 27 | 28 | w._gaq || (w._gaq = []); 29 | eventer(messageEvent, function (e) { 30 | if (e.origin === base && (e.data === "vote" || e.data === "submit")) { 31 | w._gaq.push(["_trackSocial", "Hacker News", e.data]); 32 | } 33 | }, false); 34 | 35 | for (j = hnAnchorElements.length - 1; j >= 0; j--) { 36 | var anchor = hnAnchorElements[j], 37 | title = anchor.getAttribute("data-title") || d.title, 38 | url = anchor.getAttribute("data-url") || w.location.href, 39 | i = d.createElement("iframe"); 40 | 41 | i.src = base + "button?title=" + encodeURIComponent(title) + "&url=" + encodeURIComponent(url); 42 | i.scrolling = "auto"; 43 | i.frameBorder = "0"; 44 | i.width = "75px"; 45 | i.height = "20px"; 46 | i.className = "hn-share-iframe"; 47 | 48 | anchor.parentNode.insertBefore(i, anchor); 49 | anchor.parentNode.removeChild(anchor); 50 | } 51 | })(window); -------------------------------------------------------------------------------- /hnbutton/button.html: -------------------------------------------------------------------------------- 1 | {{define "button"}} 2 | 3 | 4 | 5 | 6 | 16 | 17 | 18 | 19 |
20 | {{if .Id}} 21 | 22 | {{else}} 23 | 24 | {{end}} 25 | 26 | 27 | 28 | 29 | 30 | 46 | 47 | 48 |
31 | 32 | 33 | 34 | 35 | 42 | 43 | 44 |
36 | {{if .Id}} 37 | {{.Points}} 38 | {{else}} 39 | submit 40 | {{end}} 41 |
45 |
49 |
50 |
51 |
52 | 53 | 54 | {{end}} -------------------------------------------------------------------------------- /hnbutton/hnbutton.go: -------------------------------------------------------------------------------- 1 | package button 2 | 3 | import ( 4 | "appengine" 5 | "appengine/urlfetch" 6 | "encoding/json" 7 | "html/template" 8 | "io/ioutil" 9 | "net/http" 10 | "net/url" 11 | "fmt" 12 | ) 13 | 14 | var buttonTemplate, _ = template.New("page").ParseFiles("hnbutton/button.html") 15 | 16 | type hnapireply struct { 17 | Hits int 18 | Results []Result 19 | } 20 | 21 | type Result struct { 22 | Item Hit 23 | } 24 | 25 | type Hit struct { 26 | Id int 27 | Points int 28 | Num_comments int 29 | Username string 30 | } 31 | 32 | func Button(w http.ResponseWriter, r *http.Request) { 33 | c := appengine.NewContext(r) 34 | c.Infof("Requested URL: %v", r.URL) 35 | 36 | defer func() { 37 | if err := recover(); err != nil { 38 | c.Errorf("%s", err) 39 | c.Errorf("%s", "Traceback: %s", r) 40 | 41 | reply := map[string]string{ 42 | "error": fmt.Sprintf("%s", err), 43 | } 44 | 45 | resp, _ := json.Marshal(reply) 46 | w.WriteHeader(500) 47 | w.Write(resp) 48 | } 49 | }() 50 | 51 | query, _ := url.ParseQuery(r.URL.RawQuery) 52 | req_url, ok_url := query["url"] 53 | req_title, ok_title := query["title"] 54 | 55 | if !ok_url || !ok_title { 56 | panic("required parameters: url, title") 57 | } 58 | 59 | c.Infof("Fetching HN data for: %s, %s\n", req_title, req_url) 60 | 61 | _, err := url.Parse(req_url[0]) 62 | if err != nil { 63 | panic("Invalid URL: " + err.Error()) 64 | } 65 | 66 | pageData := "http://api.thriftdb.com/api.hnsearch.com/items/_search?filter[fields][url][]=" + req_url[0] 67 | 68 | client := urlfetch.Client(c) 69 | resp, err := client.Get(pageData) 70 | if err != nil { 71 | panic("Cannot fetch HN data: " + err.Error()) 72 | } 73 | 74 | defer resp.Body.Close() 75 | body, _ := ioutil.ReadAll(resp.Body) 76 | 77 | var hnreply hnapireply 78 | if err := json.Unmarshal(body, &hnreply); err != nil { 79 | panic("Cannot unmarshall JSON data") 80 | } 81 | 82 | // Cache the response in the HTTP edge cache, if possible 83 | // http://code.google.com/p/googleappengine/issues/detail?id=2258 84 | w.Header().Set("Cache-Control", "public, max-age=61;") 85 | 86 | if hnreply.Hits == 0 { 87 | c.Infof("No hits, rendering submit template") 88 | params := map[string]interface{}{"Url": req_url[0], "Title": req_title[0]} 89 | if err := buttonTemplate.ExecuteTemplate(w, "button", params); err != nil { 90 | panic("Cannot execute template") 91 | } 92 | 93 | } else { 94 | c.Infof("Hits: %f, Points: %f, ID: %i \n", 95 | hnreply.Hits, 96 | hnreply.Results[0].Item.Points, 97 | hnreply.Results[0].Item.Id) 98 | 99 | if err := buttonTemplate.ExecuteTemplate(w, "button", hnreply.Results[0].Item); err != nil { 100 | panic("Cannot execute template") 101 | } 102 | } 103 | } 104 | 105 | func Redirect(w http.ResponseWriter, r *http.Request) { 106 | http.Redirect(w, r, "https://github.com/igrigorik/hackernews-button", http.StatusFound) 107 | } 108 | 109 | func init() { 110 | http.HandleFunc("/button", Button) 111 | http.HandleFunc("/", Redirect) 112 | } 113 | --------------------------------------------------------------------------------