Thời đại công nghệ ta không thể mãi dậm chân tại chỗ, blogger cũng không ngoại lệ khi liên tục cập nhật thêm tiện ích, chức năng mới. Theo yêu cầu của mấy bạn trong loạt bài tới đây tôi sẽ hướng dẫn các bạn cách cập nhật tiện ích bài đăng (Blog1) lên version 2 để sử dụng được nhiều hơn thẻ gọi dữ liệu và rất nhiều chức năng thú vị khác nữa
Trước khi bắt đầu loạt bài này, nếu như bạn còn yếu về thẻ điều kiện, thẻ gọi dữ liệu, thẻ định nghĩa, thì tôi khuyên bạn không nên đụng tay vào nâng cấp vì khi nâng cấp đòi hỏi lượng kiến thức rất lớn, hướng dẫn cũng chỉ mang tính tương đối chứ không chính xác tuyệt đối vì mỗi template đều có cách trình bày khác nhau.
Qua loạt bài viết này, bạn không những upgrade được blog1 lên v2 mà sẽ biết thêm được cách thiết kế và hiển thị bài đăng cho template blogger
Trong phần 1 này tôi sẽ hướng dẫn bạn hiển thị lại bài đăng ở trang bài viết và trang tĩnh.
Đầu tiên bạn backup lại template cũ để tránh lỗi phát sinh và sẽ lấy dữ liệu blog1 cũ chuyển sang blog1 mới
Để cập nhật template lên layout v3 và widget v2 bạn làm như sau
Thêm b:defaultwidgetversion='2' b:layoutsVersion='3' vào thẻ <html...>
Chèn code markup vào sau thẻ </b:skin>
<b:defaultmarkups> <b:defaultmarkup type='Common'> <b:includable id='widgetNotAvailableInPreview'> <b:if cond='data:widget.type == "AdSense"'> <div class='vertical-ad-placeholder'> <span><b:message name='messages.adsGoHere'/></span> </div> <b:else/> <b:include name='super.widgetNotAvailableInPreview'/> </b:if> </b:includable> </b:defaultmarkup> <b:defaultmarkup type='AdSense,Blog'> <b:includable id='defaultAdUnit'> <b:comment>Clear out style (needs to be a non-empty string)</b:comment> <b:with value='"/* Done in css. */"' var='style'> <b:include name='super.defaultAdUnit'/> </b:with> </b:includable> </b:defaultmarkup> <b:defaultmarkup type='Blog,FeaturedPost'> <b:includable id='headerByline'> <b:include cond='data:view.isMultipleItems or data:widgets.Blog.first.headerByline.items.share' data='{ shareButtonClass: "post-share-buttons-top", overridden: true }' name='maybeAddShareButtons'/> <b:include name='super.headerByline'/> </b:includable> </b:defaultmarkup> <b:defaultmarkup type='Blog,FeaturedPost,PopularPosts'> <b:includable id='commentsLink'> <a class='comment-link' expr:href='data:post.commentsUrl' expr:onclick='data:post.commentsUrlOnclick'> <b:include data='{ iconClass: "touch-icon" }' name='commentIcon'/> <span class='num_comments'> <b:if cond='data:post.numberOfComments > 0'> <b:message name='messages.numberOfComments'> <b:param expr:value='data:post.numberOfComments' name='numComments'/> </b:message> <b:else/> <data:messages.postAComment/> </b:if> </span> </a> </b:includable> <b:includable id='snippetedPostByline'> <b:include name='headerByline'/> </b:includable> <b:includable id='postLabels'> <b:comment>We don't display labels on the home page.</b:comment> <b:if cond='data:view.isSingleItem and data:widget.type == "Blog"'> <b:include name='super.postLabels'/> </b:if> </b:includable> <b:includable id='postShareButtons' var='post'> <b:comment>We call super.postShareButtons from the migrated positions.</b:comment> </b:includable> <b:includable id='postJumpLink'> <b:comment>Overridden, and migrated to postFooter. Called as postFooterJumpLink.</b:comment> </b:includable> <b:includable id='postFooterJumpLink'> <b:comment>Ripple, and show 'keep reading' as the default.</b:comment> <div class='jump-link flat-button ripple'> <a expr:href='data:post.hasJumpLink ? data:post.url fragment "more" : data:post.url' expr:title='data:post.title'> <data:blog.jumpLinkMessage/> </a> </div> </b:includable> <b:includable id='postFooter' var='post'> <div class='post-bottom'> <div class='post-footer float-container'> <b:include name='footerBylines'/> <b:include cond='data:widget.type == "Blog"' data='post' name='postFooterAuthorProfile'/> </div> <b:if cond='data:view.isSingleItem'> <b:include data='{ shareButtonClass: "post-share-buttons-bottom invisible", overridden: true }' name='maybeAddShareButtons'/> <b:else/> <b:include data='post' name='postFooterJumpLink'/> </b:if> </div> </b:includable> </b:defaultmarkup> <b:defaultmarkup type='Blog'> <b:includable id='main'> <b:include name='noContentPlaceholder'/> <b:comment>Cap the total number of ads (widgets and inline ads).</b:comment> <b:with value='3' var='maxNumAds'> <b:with value='data:widgets.AdSense.size' var='numDesktopAds'> <b:with value='data:widgets.AdSense count (w => w.sectionId != "ads")' var='numMobileAds'> <b:comment>Filter out the featured post, but only on the homepage.</b:comment> <b:with value='data:widgets.FeaturedPost filter (w => w.sectionId == "page_body") map (w => w.postId)' var='featuredPostIds'> <b:with value='data:view.isHomepage ? data:posts filter (post => post.id not in data:featuredPostIds) : data:posts' var='posts'> <b:include name='super.main'/> </b:with> </b:with> </b:with> </b:with> </b:with> </b:includable> <b:includable id='feedLinks'> <b:comment>Don't show feed links.</b:comment> </b:includable> <b:includable id='postBodySnippet' var='post'> <div class='container post-body entry-content' expr:id='"post-snippet-" + data:post.id'> <b:if cond='data:post.featuredImage'> <div class='snippet-thumbnail'> <b:include data='{ image: data:post.featuredImage, imageSizes: [32, 64, 128, 256], imageRatio: "1:1", sourceSizes: "(max-width: 800px) 20vw, 128px" }' name='responsiveImage'/> </div> </b:if> <b:include cond='data:post' data='post' name='postSnippet'/> </div> </b:includable> <b:includable id='previousPageLink'> <b:comment>Don't show</b:comment> </b:includable> <b:includable id='homePageLink'> <b:comment>Don't show</b:comment> </b:includable> <b:includable id='nextPageLink'> <a class='blog-pager-older-link flat-button ripple' expr:href='data:olderPageUrl' expr:title='data:messages.morePosts'> <data:messages.morePosts/> </a> </b:includable> <b:includable id='inlineAd' var='post'> <div> <b:class cond='data:post.adNumber + data:numDesktopAds lt data:maxNumAds' name='desktop-ad'/> <b:class cond='data:post.adNumber + data:numMobileAds lt data:maxNumAds' name='mobile-ad'/> <b:include data='post' name='super.inlineAd'/> </div> </b:includable> </b:defaultmarkup> <b:defaultmarkup type='BlogArchive'> <b:includable id='main' var='this'> <details class='collapsible extendable'> <b:attr cond='data:view.isArchive' name='open' value='open'/> <b:with value='true' var='renderAsDetails'> <b:with value='data:messages.archive' var='defaultTitle'> <b:include name='super.main'/> </b:with> </b:with> </details> </b:includable> <b:includable id='flat'> <b:include data='{ buttonClass: "pill-button", items: data:this.data, itemSet: "data", itemsMarkup: "super.flat" }' name='extendableItems'/> </b:includable> <b:includable id='hierarchy'> <b:include data='{ buttonClass: "pill-button", limit: 1, items: data:this.data, itemSet: "data", itemsMarkup: "super.hierarchy" }' name='extendableItems'/> </b:includable> </b:defaultmarkup> <b:defaultmarkup type='BlogSearch'> <b:includable id='searchSubmit'> <input class='search-action flat-button' expr:value='data:messages.search.escaped' type='submit'/> </b:includable> </b:defaultmarkup> <b:defaultmarkup type='Label'> <b:includable id='main' var='this'> <details class='collapsible extendable'> <b:attr cond='data:view.isLabelSearch' name='open' value='open'/> <b:with value='true' var='renderAsDetails'> <b:with value='data:messages.labels' var='defaultTitle'> <b:include name='super.main'/> </b:with> </b:with> </details> </b:includable> <b:includable id='list'> <b:include data='{ buttonClass: "pill-button", items: data:this.labels, itemSet: "labels", itemsMarkup: "super.list" }' name='extendableItems'/> </b:includable> <b:includable id='cloud'> <b:include data='{ buttonClass: "pill-button", items: data:this.labels, itemSet: "labels", itemsMarkup: "super.cloud" }' name='extendableItems'/> </b:includable> </b:defaultmarkup> <b:defaultmarkup type='FeaturedPost'> <b:includable id='snippetedPostContent'> <b:comment>Re-order the thumbnail before the snippet, add 'Keep reading' link.</b:comment> <b:include cond='data:this.postDisplay.showTitle' name='snippetedPostTitle'/> <b:include name='headerByline'/> <b:include cond='data:this.postDisplay.showFeaturedImage and data:post.featuredImage' data='post' name='snippetedPostThumbnail'/> <b:include cond='data:this.postDisplay.showSnippet' data='post' name='postSnippet'/> <b:include data='post' name='postFooter'/> </b:includable> <b:includable id='snippetedPostThumbnail'> <div class='snippet-thumbnail'> <b:with value='data:post.featuredImage.isYoutube ? resizeImage(data:post.featuredImage.youtubeMaxResDefaultUrl, 945, "945:600") : ""' var='highRes'> <b:include data='{ image: data:post.featuredImage, imageSizes: [256, 512, 945, 1684], imageRatio: "945:600", sourceSizes: "(min-width: 954px) 842px, (min-width: 801px) calc(100vw - 112px), calc(100vw - 64px)", enhancedSourceset: data:highRes }' name='responsiveImage'/> </b:with> </div> </b:includable> </b:defaultmarkup> <b:defaultmarkup type='Header'> <b:includable id='image'> <b:include name='super.image'/> <b:comment>If we are replacing the title, force it to render anyway, and it'll be hidden in CSS.</b:comment> <b:include cond='data:this.imagePlacement == "REPLACE"' name='title'/> </b:includable> <b:includable id='title'> <div> <b:class cond='data:this.imagePlacement == "REPLACE"' name='replaced'/> <b:include name='super.title'/> </div> </b:includable> </b:defaultmarkup> <b:defaultmarkup type='PopularPosts'> <b:includable id='main' var='this'> <b:comment>Default the title to 'Popular posts from this blog'.</b:comment> <b:with value='data:messages.popularPostsFromThisBlog' var='defaultTitle'> <b:include name='super.main'/> </b:with> </b:includable> <b:includable id='snippetedPostContent'> <b:comment>Add a 'keep reading' link to the item-content.</b:comment> <b:include name='snippetedPostTitle'/> <b:include name='snippetedPostByline'/> <div class='item-content float-container'> <b:include cond='data:this.postDisplay.showFeaturedImage and data:post.featuredImage' name='snippetedPostThumbnail'/> <b:if cond='data:this.postDisplay.showSnippet'> <b:with value='"popular-posts"' var='snippetPrefix'> <b:include cond='data:post' data='post' name='postSnippet'/> </b:with> </b:if> <b:include data='post' name='postFooterJumpLink'/> </div> </b:includable> </b:defaultmarkup> <b:defaultmarkup type='PageList'> <b:includable id='content'> <div class='widget-content'> <b:include cond='data:widget.sectionId == "page_list_top"' name='overflowablePageList'/> <b:include cond='data:widget.sectionId != "page_list_top"' name='pageList'/> </div> </b:includable> <b:includable id='overflowButton'> <a><data:messages.moreEllipsis/></a> </b:includable> </b:defaultmarkup> <b:defaultmarkup type='Profile'> <b:includable id='main' var='this'> <div class='wrapper'> <b:class cond='!data:this.team' name='solo'/> <b:comment>No title for single profiles. Default to 'Blog authors' for team.</b:comment> <b:with value='data:messages.blogAuthors' var='defaultTitle'> <b:include cond='data:this.team' name='widget-title'/> </b:with> <b:include name='content'/> </div> </b:includable> <b:includable id='defaultProfileImage'> <div class='default-avatar-wrapper'> <b:include data='{ iconClass: "avatar-icon" }' name='defaultAvatarIcon'/> </div> </b:includable> <b:includable id='userProfileText'> <dd class='profile-textblock profile-snippet snippet-container r-snippet-container'> <div class='snippet-item r-snippetized'> <data:aboutme/> </div> <div class='snippet-fade r-snippet-fade hidden'/> </dd> </b:includable> <b:includable id='viewProfileLink'> <b:comment>Change link to 'visit profile'</b:comment> <a class='profile-link visit-profile pill-button' expr:href='data:userUrl' rel='author'> <data:messages.visitProfile/> </a> </b:includable> </b:defaultmarkup> </b:defaultmarkups>
Chuyển tới tiện ích Blog1 và thay toàn bộ code Blog1 cũ (bắt đầu từ <b:widget...> và kết thúc bằng </b:widget>) bằng code sau
<b:widget id='Blog1' locked='true' title='Bài đăng trên Blog' type='Blog' version='2' visible='true'> <b:widget-settings> <b:widget-setting name='showDateHeader'>false</b:widget-setting> <b:widget-setting name='style.textcolor'>#0be6af</b:widget-setting> <b:widget-setting name='showShareButtons'>false</b:widget-setting> <b:widget-setting name='authorLabel'>Bởi</b:widget-setting> <b:widget-setting name='showCommentLink'>true</b:widget-setting> <b:widget-setting name='style.urlcolor'>#0be6af</b:widget-setting> <b:widget-setting name='showAuthor'>true</b:widget-setting> <b:widget-setting name='style.linkcolor'>#0be6af</b:widget-setting> <b:widget-setting name='style.unittype'>TextAndImage</b:widget-setting> <b:widget-setting name='style.bgcolor'>#000000</b:widget-setting> <b:widget-setting name='timestampLabel'/> <b:widget-setting name='showAuthorProfile'>false</b:widget-setting> <b:widget-setting name='style.layout'>1x1</b:widget-setting> <b:widget-setting name='showLabels'>true</b:widget-setting> <b:widget-setting name='showLocation'>false</b:widget-setting> <b:widget-setting name='postLabelsLabel'/> <b:widget-setting name='showTimestamp'>true</b:widget-setting> <b:widget-setting name='postsPerAd'>1</b:widget-setting> <b:widget-setting name='showBacklinks'>false</b:widget-setting> <b:widget-setting name='style.bordercolor'>#000000</b:widget-setting> <b:widget-setting name='showInlineAds'>false</b:widget-setting> <b:widget-setting name='showReactions'>false</b:widget-setting> </b:widget-settings> <b:includable id='main' var='this' /> <b:includable id='aboutPostAuthor' /> <b:includable id='addComments' /> <b:includable id='commentAuthorAvatar' /> <b:includable id='commentDeleteIcon' var='comment' /> <b:includable id='commentForm' var='post' /> <b:includable id='commentFormIframeSrc' var='post' /> <b:includable id='commentItem' var='comment' /> <b:includable id='commentList' var='comments' /> <b:includable id='commentPicker' var='post' /> <b:includable id='comments' var='post' /> <b:includable id='commentsLink' /> <b:includable id='commentsTitle' /> <b:includable id='defaultAdUnit' /> <b:includable id='feedLinks' /> <b:includable id='feedLinksBody' var='links' /> <b:includable id='headerByline' /> <b:includable id='homePageLink' /> <b:includable id='iframeComments' var='post' /> <b:includable id='inlineAd' var='post' /> <b:includable id='nextPageLink' /> <b:includable id='post' var='post' /> <b:includable id='postBody' var='post' /> <b:includable id='postBodySnippet' var='post' /> <b:includable id='postCommentsAndAd' var='post' /> <b:includable id='postCommentsLink' /> <b:includable id='postFooter' var='post' /> <b:includable id='postFooterAuthorProfile' var='post' /> <b:includable id='postFooterJumpLink' /> <b:includable id='postHeader' var='post' /> <b:includable id='postJumpLink' var='post' /> <b:includable id='postLabels' /> <b:includable id='postMeta' var='post' /> <b:includable id='postMetadataJSON' var='post' /> <b:includable id='postPagination' /> <b:includable id='postShareButtons' var='post' /> <b:includable id='postTitle' var='post' /> <b:includable id='previousPageLink' /> <b:includable id='snippetedPostByline' /> <b:includable id='threadedCommentForm' var='post' /> <b:includable id='threadedCommentJs' var='post' /> <b:includable id='threadedComments' var='post' /> </b:widget>
Đây là bộ khung cơ bản của tiện ích blog1 v2, chưa có dữ liệu. Khi lưu mẫu và chuyển sang xem bố cục bạn sẽ thấy nó đã lên v2
Tôi sẽ nói sơ qua về thẻ b:includable, bạn có thể liên tưởng nó như là một function trong lập trình.
* Ví dụ định nghĩa 1 "function" không sử dụng biến
<b:includable id='xxx'>//Nội dung</b:includable>
gọi để sử dụng: <b:include name='xxx'/>
* Sử dụng biến
<b:includable id='xxx' var='yyy'>//Nội dung</b:includable>
gọi để sử dụng: <b:include data='yyy' name='xxx'/>
Như vậy trong tiện ích Blog1 bạn có thể tạo nhiều thẻ b:includable và gọi chúng qua lại, nếu như bạn không định nghĩa mà đã gọi tiện ích Blog1 sẽ bị hỏng và nội dung của nó sẽ trắng xóa.
Trước tiên gọi dữ liệu trong <b:includable id='main' var='this'>
<b:includable id='main' var='this'> <div class='blog-posts hfeed container'> <b:loop index='i' values='data:posts' var='post'> <b:include data='post' name='postCommentsAndAd' /> </b:loop> </div> </b:includable>
Như bạn thấy nó đã gọi đến một thẻ nữa là <b:includable id='postCommentsAndAd' var='post'>, tiếp tục chèn code
<b:includable id='postCommentsAndAd' var='post'> <article class='post-outer-container'> <div class='post-outer'> <b:include data='post' name='post' /> </div> </article> </b:includable>
Nó lại gọi tới thẻ <b:includable id='post' var='post'>. Dừng lại ở đây một chút vì thẻ này rất quan trọng.
Với trang bài viết cấu trúc thường là breadcrumb / tiêu đề / mô tả / nội dung / nhãn / tác giả /.../ bình luận
Với bộ khung tôi trình bày ở trên bạn có thể thấy v2 này phân chia ra rất rõ từng thành phần, ta chỉ cần tách code ở blog1 cũ và dán trở lại. Viết tiếp code để gọi dữ liệu cho trang tĩnh và trang bài viết
<b:includable id='post' var='post'> <div class='post'> <b:if cond='data:view.isPost'> <b:include data='post' name='postTitle' /> <b:include data='post' name='postMeta' /> <b:include data='post' name='postBody' /> <b:include data='post' name='postFooter' /> <b:else/> <b:if cond='data:view.isPage'> <b:include data='post' name='postTitle' /> <b:include data='post' name='postBody' /> </b:if> </b:if> </div> </b:includable>
Lần lượt chèn lại code cho các thẻ mà nó đã gọi tới (với mục tôi đánh dấu là minh họa thì bạn phải lấy code trong blog1 cũ của bạn nhé)
+ Tiêu đề bài đăng (minh họa)
<b:includable id='postTitle' var='post'> <b:if cond='data:view.isPost'> <b:if cond='data:post.title'> <div class='post-head'> <h1 class='post-title entry-title' itemprop='name headline'> <b:if cond='data:post.link'> <a expr:href='data:post.link'><data:post.title/></a> <b:else/> <b:if cond='data:post.url'> <b:if cond='data:blog.url != data:post.url'> <a expr:href='data:post.url'><data:post.title/></a> <b:else/> <data:post.title/> </b:if> <b:else/> <data:post.title/> </b:if> </b:if> </h1> </div> </b:if> </b:if> <b:if cond='data:view.isPage'> <b:if cond='data:post.title'> <div class='post-head'> <h1 class='post-title entry-title' itemprop='name'> <b:if cond='data:post.link'> <a expr:href='data:post.link'><data:post.title/></a> <b:else/> <b:if cond='data:post.url'> <b:if cond='data:blog.url != data:post.url'> <a expr:href='data:post.url'><data:post.title/></a> <b:else/> <data:post.title/> </b:if> <b:else/> <data:post.title/> </b:if> </b:if> </h1> </div> </b:if> </b:if> </b:includable>
+ Mô tả (minh họa)
<b:if cond='data:view.isPost'> <div class='post-meta'> <span class='post-author vcard'> <b:if cond='data:post.author.profileUrl'> <span class='fn' itemprop='author' itemscope='itemscope' itemtype='http://schema.org/Person'><meta expr:content='data:post.author.profileUrl' itemprop='url'/>by <a class='g-profile' expr:href='data:post.author.profileUrl' expr:title='data:post.author.name' rel='author'><span itemprop='name'><strong><data:post.author.name></strong></span></a> </span> </b:if> </span> <span class='post-timestamp'> <b:if cond='data:post.url'> <meta expr:content='data:post.canonicalUrl' itemprop='url mainEntityOfPage'/> <a class='timestamp-link' expr:href='data:post.url' rel='bookmark' title='permanent link'><abbr class='published timeago' expr:title='data:post.date' itemprop='datePublished dateModified'><data:post.date/></abbr> </a> </b:if> </span> </div> </b:if>
Trong phần này này xuất hiện các thẻ dữ liệu như data:post.author.profileUrl,data:post.date,... ? Đó là sự khác biệt giữa v1 và v2, mặc dù chúng cho cùng một giá trị nhưng cách thức gọi dữ liệu đã có sự thay đổi, vì thế nên đầu bài viết tôi đã nói nếu chưa nắm chắc thẻ gọi dữ liệu sẽ rất khó để làm được vì dù có đúng theo chỉ dẫn nhưng sai thẻ gọi dữ liệu thì hiện trên trang sẽ chỉ là màu trắng xóa. Đọc bài Thẻ gọi dữ liệu tiện ích bài đăng Blog1 để lấy tất cả thẻ gọi dữ liệu áp dụng cho v2 và cập nhật lại khi copy code từ blog v1 lên blog v2
+ Nội dung bài đăng
<b:includable id='postBody' var='post'> <div class='post-body entry-content float-container' expr:id='"post-body-" + data:post.id'> <data:post.body/> </div> </b:includable>
* Post footer
<b:includable id='postFooter' var='post'> <b:include data='post' name='postLabels' /> <div class='share-art'>...</div> <b:include data='post' name='postFooterAuthorProfile' /> <div id='related-posts'>...</div> </b:includable>
Định nghĩa <b:includable id='postLabels'> (minh họa)
<b:includable id='postLabels'> <div class='label-head'> <span>Tags :</span> <b:if cond='data:post.labels'> <b:loop values='data:post.labels' var='label'> <a expr:href='data:label.url' rel='tag'><data:label.name/></a> </b:loop> </b:if> </div> </b:includable>
Định nghĩa <b:includable id='postFooterAuthorProfile' var='post'> (minh họa)
<b:includable id='postFooterAuthorProfile' var='post'> <div>Xin chào các bạn tôi là abcdxyz...</div> </b:includable>
**Lưu ý quan trọng: Trong v2 có một số thành phần thêm vào cũng như một số bị loại bỏ đi lấy ví dụ
Khi bạn copy code từ v1 sang v2
<b:if cond='data:post.labels'> <b:loop values='data:post.labels' var='label'> <b:if cond='data:label.isLast == "true"'> <a class='hide-label'><data:label.name/></a> </b:if> </b:loop> </b:if>
Do màu đỏ không có trong cấu trúc của v2, nên màu xanh không được thực thi, hay nói cách khác là ra màu trắng tinh :)) Vì vậy bạn phải đặc biệt chú ý tới cấu trúc json và sự khác nhau giữa thẻ gọi dữ liệu giữa 2 phiên bản
Xong phần 1. Hẹn gặp lại bạn ở phần 2
1. Không vi phạm luật pháp nước CHXHCN Việt Nam
2. Không vi phạm thuần phong mỹ tục Việt Nam
3. Không bàn luận vấn đề liên quan đến tôn giáo, chính trị
4. Không đả kích, chửi bới hay đưa ra những lời nói không phù hợp với mục tiêu của website
5. Không bình luận với mục đích quảng cáo, trao đổi, mua bán
6. Khuyến khích sử dụng Tiếng Việt có dấu, hạn chế sử dụng tiếng lóng, viết tắt
7. Khi cần sự trợ giúp, vui lòng miêu tả chi tiết lỗi và để lại link đính kèm, tránh nói chung chung gây mất thời gian cho đôi bên
https://adamz20.blogspot.com/